diff --git a/AUTHORS b/AUTHORS index 7e771c2c4d..45a0544e65 100644 --- a/AUTHORS +++ b/AUTHORS @@ -93,7 +93,7 @@ answer newbie questions, and generally made Django that much better: Simon Blanchard David Blewett Matt Boersma - boobsd@gmail.com + Artem Gnilov Matías Bordese Nate Bragg Sean Brant @@ -211,7 +211,6 @@ answer newbie questions, and generally made Django that much better: Dimitris Glezos glin@seznam.cz martin.glueck@gmail.com - Artyom Gnilov Ben Godfrey GomoX Guilherme Mesquita Gondim @@ -347,6 +346,7 @@ answer newbie questions, and generally made Django that much better: Frantisek Malina Mike Malone Martin Maney + Michael Manfre masonsimon+django@gmail.com Manuzhai Petr Marhoun @@ -401,6 +401,7 @@ answer newbie questions, and generally made Django that much better: Christian Oudard oggie rob oggy + Jens Page Jay Parlar Carlos Eduardo de Paula John Paulett diff --git a/django/contrib/admin/static/admin/js/actions.js b/django/contrib/admin/static/admin/js/actions.js index 94aa6db934..8494970aa1 100644 --- a/django/contrib/admin/static/admin/js/actions.js +++ b/django/contrib/admin/static/admin/js/actions.js @@ -3,7 +3,7 @@ var options = $.extend({}, $.fn.actions.defaults, opts); var actionCheckboxes = $(this); var list_editable_changed = false; - checker = function(checked) { + var checker = function(checked) { if (checked) { showQuestion(); } else { @@ -11,7 +11,7 @@ } $(actionCheckboxes).attr("checked", checked) .parent().parent().toggleClass(options.selectedClass, checked); - } + }, updateCounter = function() { var sel = $(actionCheckboxes).filter(":checked").length; $(options.counterContainer).html(interpolate( @@ -29,30 +29,30 @@ } return value; }); - } + }, showQuestion = function() { $(options.acrossClears).hide(); $(options.acrossQuestions).show(); $(options.allContainer).hide(); - } + }, showClear = function() { $(options.acrossClears).show(); $(options.acrossQuestions).hide(); $(options.actionContainer).toggleClass(options.selectedClass); $(options.allContainer).show(); $(options.counterContainer).hide(); - } + }, reset = function() { $(options.acrossClears).hide(); $(options.acrossQuestions).hide(); $(options.allContainer).hide(); $(options.counterContainer).show(); - } + }, clearAcross = function() { reset(); $(options.acrossInput).val(0); $(options.actionContainer).removeClass(options.selectedClass); - } + }; // Show counter by default $(options.counterContainer).show(); // Check state of checkboxes and reinit state if needed @@ -81,9 +81,9 @@ }); lastChecked = null; $(actionCheckboxes).click(function(event) { - if (!event) { var event = window.event; } + if (!event) { event = window.event; } var target = event.target ? event.target : event.srcElement; - if (lastChecked && $.data(lastChecked) != $.data(target) && event.shiftKey == true) { + if (lastChecked && $.data(lastChecked) != $.data(target) && event.shiftKey === true) { var inrange = false; $(lastChecked).attr("checked", target.checked) .parent().parent().toggleClass(options.selectedClass, target.checked); @@ -124,7 +124,7 @@ } } }); - } + }; /* Setup plugin defaults */ $.fn.actions.defaults = { actionContainer: "div.actions", @@ -135,5 +135,5 @@ acrossClears: "div.actions span.clear", allToggle: "#action-toggle", selectedClass: "selected" - } + }; })(django.jQuery); diff --git a/django/contrib/admin/static/admin/js/actions.min.js b/django/contrib/admin/static/admin/js/actions.min.js index c4a01e4f6e..2c6300094f 100644 --- a/django/contrib/admin/static/admin/js/actions.min.js +++ b/django/contrib/admin/static/admin/js/actions.min.js @@ -1,7 +1,6 @@ -(function(a){a.fn.actions=function(g){var b=a.extend({},a.fn.actions.defaults,g),f=a(this),e=!1;checker=function(c){c?showQuestion():reset();a(f).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)};updateCounter=function(){var c=a(f).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},!0));a(b.allToggle).attr("checked",function(){c==f.length?(value=!0,showQuestion()):(value= -!1,clearAcross());return value})};showQuestion=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()};showClear=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()};reset=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()};clearAcross=function(){reset();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)}; -a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass);updateCounter();1==a(b.acrossInput).val()&&showClear()});a(b.allToggle).show().click(function(){checker(a(this).attr("checked"));updateCounter()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);showClear()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",!1);clearAcross();checker(0); -updateCounter()});lastChecked=null;a(f).click(function(c){if(!c)c=window.event;var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&!0==c.shiftKey){var e=!1;a(lastChecked).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(f).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))e=e?!1:!0;e&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass, -d.checked);lastChecked=d;updateCounter()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){e=!0});a('form#changelist-form button[name="index"]').click(function(){if(e)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))});a('form#changelist-form input[name="_save"]').click(function(){var b=!1;a("div.actions select option:selected").each(function(){a(this).val()&&(b=!0)}); -if(b)return e?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})};a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across", -acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery); +(function(a){a.fn.actions=function(m){var b=a.extend({},a.fn.actions.defaults,m),f=a(this),e=!1,j=function(c){c?h():i();a(f).attr("checked",c).parent().parent().toggleClass(b.selectedClass,c)},g=function(){var c=a(f).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},!0));a(b.allToggle).attr("checked",function(){c==f.length?(value=!0,h()):(value=!1,k());return value})},h=function(){a(b.acrossClears).hide(); +a(b.acrossQuestions).show();a(b.allContainer).hide()},l=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},i=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},k=function(){i();a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)};a(b.counterContainer).show();a(this).filter(":checked").each(function(){a(this).parent().parent().toggleClass(b.selectedClass); +g();1==a(b.acrossInput).val()&&l()});a(b.allToggle).show().click(function(){j(a(this).attr("checked"));g()});a("div.actions span.question a").click(function(c){c.preventDefault();a(b.acrossInput).val(1);l()});a("div.actions span.clear a").click(function(c){c.preventDefault();a(b.allToggle).attr("checked",!1);k();j(0);g()});lastChecked=null;a(f).click(function(c){c||(c=window.event);var d=c.target?c.target:c.srcElement;if(lastChecked&&a.data(lastChecked)!=a.data(d)&&!0===c.shiftKey){var e=!1;a(lastChecked).attr("checked", +d.checked).parent().parent().toggleClass(b.selectedClass,d.checked);a(f).each(function(){if(a.data(this)==a.data(lastChecked)||a.data(this)==a.data(d))e=e?false:true;e&&a(this).attr("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);lastChecked=d;g()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){e=!0});a('form#changelist-form button[name="index"]').click(function(){if(e)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))}); +a('form#changelist-form input[name="_save"]').click(function(){var b=!1;a("div.actions select option:selected").each(function(){a(this).val()&&(b=!0)});if(b)return e?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})}; +a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery); diff --git a/django/contrib/admin/static/admin/js/inlines.js b/django/contrib/admin/static/admin/js/inlines.js index bddd6f75ba..668e1157c8 100644 --- a/django/contrib/admin/static/admin/js/inlines.js +++ b/django/contrib/admin/static/admin/js/inlines.js @@ -31,11 +31,11 @@ } }; var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); - var nextIndex = parseInt(totalForms.val()); + 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; + var showAddButton = maxForms.val() === '' || (maxForms.val()-totalForms.val()) > 0; $(this).each(function(i) { $(this).not("." + options.emptyCssClass).addClass(options.formCssClass); }); @@ -52,13 +52,14 @@ $(this).filter(":last").after('"); addButton = $(this).filter(":last").next().find("a"); } - addButton.click(function() { + 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); + .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: @@ -78,14 +79,15 @@ // Insert the new form when it has been fully edited row.insertBefore($(template)); // Update number of total forms - $(totalForms).val(parseInt(totalForms.val()) + 1); + $(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) { + 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() { + 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(); @@ -98,7 +100,7 @@ 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) { + if ((maxForms.val() === '') || (maxForms.val()-forms.length) > 0) { addButton.parent().show(); } // Also, update names and ids for all remaining form controls @@ -110,17 +112,15 @@ updateElementIndex(this, options.prefix, i); }); } - return false; }); // If a post-add callback was supplied, call it with the added form: if (options.added) { options.added(row); } - return false; }); } return this; - } + }; /* Setup plugin defaults */ $.fn.formset.defaults = { prefix: "form", // The form prefix for your django formset @@ -132,5 +132,5 @@ formCssClass: "dynamic-form", // CSS class applied to each form in a formset added: null, // Function called each time a new form is added removed: null // Function called each time a form is deleted - } + }; })(django.jQuery); diff --git a/django/contrib/admin/static/admin/js/inlines.min.js b/django/contrib/admin/static/admin/js/inlines.min.js index 5631cd5cf2..53dd92f2f9 100644 --- a/django/contrib/admin/static/admin/js/inlines.min.js +++ b/django/contrib/admin/static/admin/js/inlines.min.js @@ -1,5 +1,5 @@ -(function(b){b.fn.formset=function(c){var a=b.extend({},b.fn.formset.defaults,c),j=function(a,e,d){var i=RegExp("("+e+"-(\\d+|__prefix__))"),e=e+"-"+d;b(a).attr("for")&&b(a).attr("for",b(a).attr("for").replace(i,e));if(a.id)a.id=a.id.replace(i,e);if(a.name)a.name=a.name.replace(i,e)},c=b("#id_"+a.prefix+"-TOTAL_FORMS").attr("autocomplete","off"),g=parseInt(c.val()),f=b("#id_"+a.prefix+"-MAX_NUM_FORMS").attr("autocomplete","off"),c=""==f.val()||0'+a.addText+""),h=b(this).parent().find("tr:last a")):(b(this).filter(":last").after('"),h=b(this).filter(":last").next().find("a"));h.click(function(){var c=b("#id_"+a.prefix+ -"-TOTAL_FORMS"),e=b("#"+a.prefix+"-empty"),d=e.clone(!0);d.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+g);d.is("tr")?d.children(":last").append('"):d.is("ul")||d.is("ol")?d.append('
  • '+a.deleteText+"
  • "):d.children(":first").append(''+a.deleteText+ -"");d.find("*").each(function(){j(this,a.prefix,c.val())});d.insertBefore(b(e));b(c).val(parseInt(c.val())+1);g+=1;""!=f.val()&&0>=f.val()-c.val()&&h.parent().hide();d.find("a."+a.deleteCssClass).click(function(){var c=b(this).parents("."+a.formCssClass);c.remove();g-=1;a.removed&&a.removed(c);c=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(c.length);(""==f.val()||0'+a.addText+""),i=b(this).parent().find("tr:last a")):(b(this).filter(":last").after('"),i=b(this).filter(":last").next().find("a"));i.click(function(c){c.preventDefault(); +var f=b("#id_"+a.prefix+"-TOTAL_FORMS"),c=b("#"+a.prefix+"-empty"),d=c.clone(true);d.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+h);d.is("tr")?d.children(":last").append('"):d.is("ul")||d.is("ol")?d.append('
  • '+a.deleteText+"
  • "):d.children(":first").append(''+ +a.deleteText+"");d.find("*").each(function(){j(this,a.prefix,f.val())});d.insertBefore(b(c));b(f).val(parseInt(f.val(),10)+1);h=h+1;g.val()!==""&&g.val()-f.val()<=0&&i.parent().hide();d.find("a."+a.deleteCssClass).click(function(e){e.preventDefault();e=b(this).parents("."+a.formCssClass);e.remove();h=h-1;a.removed&&a.removed(e);e=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(e.length);(g.val()===""||g.val()-e.length>0)&&i.parent().show();for(var c=0,d=e.length;c - - +
    {% blocktrans with name=app.name %}{{ name }}{% endblocktrans %}
    + {% for model in app.models %} {% if model.admin_url %} diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index bc158da80a..85e03f3b75 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,4 +1,5 @@ import operator +from functools import reduce from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.core.paginator import InvalidPage diff --git a/django/contrib/auth/management/commands/createsuperuser.py b/django/contrib/auth/management/commands/createsuperuser.py index ad659773f8..f3f1a7b671 100644 --- a/django/contrib/auth/management/commands/createsuperuser.py +++ b/django/contrib/auth/management/commands/createsuperuser.py @@ -80,7 +80,7 @@ class Command(BaseCommand): if default_username and username == '': username = default_username if not RE_VALID_USERNAME.match(username): - sys.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.\n") + self.stderr.write("Error: That username is invalid. Use only letters, digits and underscores.") username = None continue try: @@ -88,7 +88,7 @@ class Command(BaseCommand): except User.DoesNotExist: break else: - sys.stderr.write("Error: That username is already taken.\n") + self.stderr.write("Error: That username is already taken.") username = None # Get an email @@ -98,7 +98,7 @@ class Command(BaseCommand): try: is_valid_email(email) except exceptions.ValidationError: - sys.stderr.write("Error: That e-mail address is invalid.\n") + self.stderr.write("Error: That e-mail address is invalid.") email = None else: break @@ -109,19 +109,19 @@ class Command(BaseCommand): password = getpass.getpass() password2 = getpass.getpass('Password (again): ') if password != password2: - sys.stderr.write("Error: Your passwords didn't match.\n") + self.stderr.write("Error: Your passwords didn't match.") password = None continue if password.strip() == '': - sys.stderr.write("Error: Blank passwords aren't allowed.\n") + self.stderr.write("Error: Blank passwords aren't allowed.") password = None continue break except KeyboardInterrupt: - sys.stderr.write("\nOperation cancelled.\n") + self.stderr.write("\nOperation cancelled.") sys.exit(1) User.objects.db_manager(database).create_superuser(username, email, password) if verbosity >= 1: - self.stdout.write("Superuser created successfully.\n") + self.stdout.write("Superuser created successfully.") diff --git a/django/contrib/auth/tests/management.py b/django/contrib/auth/tests/management.py index 5d31bd70a4..81ab0aa052 100644 --- a/django/contrib/auth/tests/management.py +++ b/django/contrib/auth/tests/management.py @@ -2,6 +2,7 @@ from StringIO import StringIO from django.contrib.auth import models, management from django.contrib.auth.management.commands import changepassword +from django.core.management.base import CommandError from django.test import TestCase @@ -56,16 +57,10 @@ class ChangepasswordManagementCommandTestCase(TestCase): def test_that_max_tries_exits_1(self): """ A CommandError should be thrown by handle() if the user enters in - mismatched passwords three times. This should be caught by execute() and - converted to a SystemExit + mismatched passwords three times. """ command = changepassword.Command() command._get_pass = lambda *args: args or 'foo' - self.assertRaises( - SystemExit, - command.execute, - "joe", - stdout=self.stdout, - stderr=self.stderr - ) + with self.assertRaises(CommandError): + command.execute("joe", stdout=self.stdout, stderr=self.stderr) diff --git a/django/contrib/flatpages/fixtures/example_site.json b/django/contrib/flatpages/fixtures/example_site.json new file mode 100644 index 0000000000..71aa84de12 --- /dev/null +++ b/django/contrib/flatpages/fixtures/example_site.json @@ -0,0 +1,11 @@ +[ + { + "pk": 1, + "model": "sites.site", + "fields": { + "domain": "example.com", + "name": "example.com" + } + } +] + diff --git a/django/contrib/flatpages/tests/csrf.py b/django/contrib/flatpages/tests/csrf.py index a8b9c2804c..e64c4bb102 100644 --- a/django/contrib/flatpages/tests/csrf.py +++ b/django/contrib/flatpages/tests/csrf.py @@ -18,9 +18,10 @@ from django.test.utils import override_settings TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), + SITE_ID=1, ) class FlatpageCSRFTests(TestCase): - fixtures = ['sample_flatpages'] + fixtures = ['sample_flatpages', 'example_site'] urls = 'django.contrib.flatpages.tests.urls' def setUp(self): diff --git a/django/contrib/flatpages/tests/forms.py b/django/contrib/flatpages/tests/forms.py index eb37e9ec29..0e54176aa2 100644 --- a/django/contrib/flatpages/tests/forms.py +++ b/django/contrib/flatpages/tests/forms.py @@ -5,7 +5,10 @@ from django.test import TestCase from django.test.utils import override_settings from django.utils import translation +@override_settings(SITE_ID=1) class FlatpageAdminFormTests(TestCase): + fixtures = ['example_site'] + def setUp(self): self.form_data = { 'title': "A test page", @@ -89,5 +92,5 @@ class FlatpageAdminFormTests(TestCase): self.assertEqual( f.errors, - {'sites': [u'This field is required.']}) + {'sites': [translation.ugettext(u'This field is required.')]}) diff --git a/django/contrib/flatpages/tests/middleware.py b/django/contrib/flatpages/tests/middleware.py index fa9d735c0c..4afa4ff982 100644 --- a/django/contrib/flatpages/tests/middleware.py +++ b/django/contrib/flatpages/tests/middleware.py @@ -19,9 +19,10 @@ from django.test.utils import override_settings TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), + SITE_ID=1, ) class FlatpageMiddlewareTests(TestCase): - fixtures = ['sample_flatpages'] + fixtures = ['sample_flatpages', 'example_site'] urls = 'django.contrib.flatpages.tests.urls' def test_view_flatpage(self): @@ -75,7 +76,7 @@ class FlatpageMiddlewareTests(TestCase): enable_comments=False, registration_required=False, ) - fp.sites.add(1) + fp.sites.add(settings.SITE_ID) response = self.client.get('/some.very_special~chars-here/') self.assertEqual(response.status_code, 200) @@ -96,9 +97,10 @@ class FlatpageMiddlewareTests(TestCase): TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), + SITE_ID=1, ) class FlatpageMiddlewareAppendSlashTests(TestCase): - fixtures = ['sample_flatpages'] + fixtures = ['sample_flatpages', 'example_site'] urls = 'django.contrib.flatpages.tests.urls' def test_redirect_view_flatpage(self): @@ -130,7 +132,7 @@ class FlatpageMiddlewareAppendSlashTests(TestCase): enable_comments=False, registration_required=False, ) - fp.sites.add(1) + fp.sites.add(settings.SITE_ID) response = self.client.get('/some.very_special~chars-here') self.assertRedirects(response, '/some.very_special~chars-here/', status_code=301) @@ -144,7 +146,7 @@ class FlatpageMiddlewareAppendSlashTests(TestCase): enable_comments=False, registration_required=False, ) - fp.sites.add(1) + fp.sites.add(settings.SITE_ID) response = self.client.get('/') self.assertEqual(response.status_code, 200) diff --git a/django/contrib/flatpages/tests/templatetags.py b/django/contrib/flatpages/tests/templatetags.py index de6a0a90fb..aebc62277e 100644 --- a/django/contrib/flatpages/tests/templatetags.py +++ b/django/contrib/flatpages/tests/templatetags.py @@ -18,6 +18,7 @@ from django.test.utils import override_settings TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), + SITE_ID=1, ) class FlatpageTemplateTagTests(TestCase): fixtures = ['sample_flatpages'] diff --git a/django/contrib/flatpages/tests/views.py b/django/contrib/flatpages/tests/views.py index 997bfabec8..b69bd6abb4 100644 --- a/django/contrib/flatpages/tests/views.py +++ b/django/contrib/flatpages/tests/views.py @@ -19,9 +19,10 @@ from django.test.utils import override_settings TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), + SITE_ID=1, ) class FlatpageViewTests(TestCase): - fixtures = ['sample_flatpages'] + fixtures = ['sample_flatpages', 'example_site'] urls = 'django.contrib.flatpages.tests.urls' def test_view_flatpage(self): @@ -85,9 +86,10 @@ class FlatpageViewTests(TestCase): TEMPLATE_DIRS=( os.path.join(os.path.dirname(__file__), 'templates'), ), + SITE_ID=1, ) class FlatpageViewAppendSlashTests(TestCase): - fixtures = ['sample_flatpages'] + fixtures = ['sample_flatpages', 'example_site'] urls = 'django.contrib.flatpages.tests.urls' def test_redirect_view_flatpage(self): @@ -119,7 +121,7 @@ class FlatpageViewAppendSlashTests(TestCase): enable_comments=False, registration_required=False, ) - fp.sites.add(1) + fp.sites.add(settings.SITE_ID) response = self.client.get('/flatpage_root/some.very_special~chars-here') self.assertRedirects(response, '/flatpage_root/some.very_special~chars-here/', status_code=301) diff --git a/django/contrib/formtools/tests/__init__.py b/django/contrib/formtools/tests/__init__.py index 88828e8a38..b5d905071f 100644 --- a/django/contrib/formtools/tests/__init__.py +++ b/django/contrib/formtools/tests/__init__.py @@ -156,9 +156,6 @@ class PreviewTests(TestCase): class FormHmacTests(unittest.TestCase): - """ - Same as SecurityHashTests, but with form_hmac - """ def test_textfield_hash(self): """ @@ -166,8 +163,8 @@ class FormHmacTests(unittest.TestCase): leading/trailing whitespace so as to be friendly to broken browsers that submit it (usually in textareas). """ - f1 = HashTestForm({'name': 'joe', 'bio': 'Nothing notable.'}) - f2 = HashTestForm({'name': ' joe', 'bio': 'Nothing notable. '}) + f1 = HashTestForm({'name': u'joe', 'bio': u'Nothing notable.'}) + f2 = HashTestForm({'name': u' joe', 'bio': u'Nothing notable. '}) hash1 = utils.form_hmac(f1) hash2 = utils.form_hmac(f2) self.assertEqual(hash1, hash2) @@ -269,10 +266,10 @@ class WizardTests(TestCase): Form should advance if the hash is present and good, as calculated using current method. """ - data = {"0-field": "test", - "1-field": "test2", - "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", - "wizard_step": "1"} + data = {"0-field": u"test", + "1-field": u"test2", + "hash_0": u"cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "wizard_step": u"1"} response = self.client.post('/wizard1/', data) self.assertEqual(2, response.context['step0']) @@ -294,18 +291,18 @@ class WizardTests(TestCase): reached[0] = True wizard = WizardWithProcessStep([WizardPageOneForm]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", - "wizard_step": "1"} + data = {"0-field": u"test", + "1-field": u"test2", + "hash_0": u"cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "wizard_step": u"1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", - "hash_1": "d5b434e3934cc92fee4bd2964c4ebc06f81d362d", - "wizard_step": "2"} + data = {"0-field": u"test", + "1-field": u"test2", + "hash_0": "cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "hash_1": u"1e6f6315da42e62f33a30640ec7e007ad3fbf1a1", + "wizard_step": u"2"} self.assertRaises(http.Http404, wizard, DummyRequest(POST=data)) def test_14498(self): @@ -324,10 +321,10 @@ class WizardTests(TestCase): wizard = WizardWithProcessStep([WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", - "wizard_step": "1"} + data = {"0-field": u"test", + "1-field": u"test2", + "hash_0": u"cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "wizard_step": u"1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) @@ -348,10 +345,10 @@ class WizardTests(TestCase): wizard = Wizard([WizardPageOneForm, WizardPageTwoForm]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", - "wizard_step": "1"} + data = {"0-field": u"test", + "1-field": u"test2", + "hash_0": u"cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "wizard_step": u"1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) @@ -374,10 +371,10 @@ class WizardTests(TestCase): wizard = WizardWithProcessStep([WizardPageOneForm, WizardPageTwoForm, WizardPageThreeForm]) - data = {"0-field": "test", - "1-field": "test2", - "hash_0": "7e9cea465f6a10a6fb47fcea65cb9a76350c9a5c", - "wizard_step": "1"} + data = {"0-field": u"test", + "1-field": u"test2", + "hash_0": u"cd13b1db3e8f55174bc5745a1b1a53408d4fd1ca", + "wizard_step": u"1"} wizard(DummyRequest(POST=data)) self.assertTrue(reached[0]) diff --git a/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py b/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py index 550622180f..37913fa078 100644 --- a/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py +++ b/django/contrib/formtools/tests/wizard/namedwizardtests/tests.py @@ -120,7 +120,7 @@ class NamedWizardTests(object): self.assertEqual(response.context['wizard']['steps'].current, 'form2') post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__) + post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), @@ -147,7 +147,7 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['form_list'] - self.assertEqual(all_data[1]['file1'].read(), open(__file__).read()) + self.assertEqual(all_data[1]['file1'].read(), open(__file__, 'rb').read()) del all_data[1]['file1'] self.assertEqual(all_data, [ {'name': u'Pony', 'thirsty': True, 'user': self.testuser}, @@ -168,7 +168,7 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__) + post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), @@ -180,7 +180,9 @@ class NamedWizardTests(object): response = self.client.get(step2_url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form2') - self.assertEqual(response.context['wizard']['form'].files['form2-file1'].read(), open(__file__).read()) + self.assertEqual( + response.context['wizard']['form'].files['form2-file1'].read(), + open(__file__, 'rb').read()) response = self.client.post( reverse(self.wizard_urlname, @@ -197,7 +199,7 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['all_cleaned_data'] - self.assertEqual(all_data['file1'].read(), open(__file__).read()) + self.assertEqual(all_data['file1'].read(), open(__file__, 'rb').read()) del all_data['file1'] self.assertEqual( all_data, @@ -221,7 +223,7 @@ class NamedWizardTests(object): self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__) + post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post( reverse(self.wizard_urlname, kwargs={'step': response.context['wizard']['steps'].current}), diff --git a/django/contrib/formtools/tests/wizard/wizardtests/tests.py b/django/contrib/formtools/tests/wizard/wizardtests/tests.py index 7afaf51f13..a9a81ba70f 100644 --- a/django/contrib/formtools/tests/wizard/wizardtests/tests.py +++ b/django/contrib/formtools/tests/wizard/wizardtests/tests.py @@ -80,7 +80,7 @@ class WizardTests(object): self.assertEqual(response.context['wizard']['steps'].current, 'form2') post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__) + post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post(self.wizard_url, post_data) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form3') @@ -93,7 +93,7 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['form_list'] - self.assertEqual(all_data[1]['file1'].read(), open(__file__).read()) + self.assertEqual(all_data[1]['file1'].read(), open(__file__, 'rb').read()) del all_data[1]['file1'] self.assertEqual(all_data, [ {'name': u'Pony', 'thirsty': True, 'user': self.testuser}, @@ -110,7 +110,7 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__) + post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post(self.wizard_url, post_data) self.assertEqual(response.status_code, 200) @@ -121,7 +121,7 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) all_data = response.context['all_cleaned_data'] - self.assertEqual(all_data['file1'].read(), open(__file__).read()) + self.assertEqual(all_data['file1'].read(), open(__file__, 'rb').read()) del all_data['file1'] self.assertEqual(all_data, { 'name': u'Pony', 'thirsty': True, 'user': self.testuser, @@ -138,7 +138,7 @@ class WizardTests(object): self.assertEqual(response.status_code, 200) post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__) + post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post(self.wizard_url, post_data) self.assertEqual(response.status_code, 200) @@ -165,7 +165,7 @@ class WizardTests(object): self.assertEqual(response.context['wizard']['steps'].current, 'form2') post_data = self.wizard_step_data[1] - post_data['form2-file1'] = open(__file__) + post_data['form2-file1'] = open(__file__, 'rb') response = self.client.post(self.wizard_url, post_data) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['wizard']['steps'].current, 'form3') diff --git a/django/contrib/formtools/utils.py b/django/contrib/formtools/utils.py index 572a0969ee..96a0092a35 100644 --- a/django/contrib/formtools/utils.py +++ b/django/contrib/formtools/utils.py @@ -1,7 +1,5 @@ -try: - import cPickle as pickle -except ImportError: - import pickle +# Do not try cPickle here (see #18340) +import pickle from django.utils.crypto import salted_hmac diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py index f709056acf..6913bf5870 100644 --- a/django/contrib/gis/admin/options.py +++ b/django/contrib/gis/admin/options.py @@ -36,6 +36,7 @@ class GeoModelAdmin(ModelAdmin): wms_url = 'http://vmap0.tiles.osgeo.org/wms/vmap0' wms_layer = 'basic' wms_name = 'OpenLayers WMS' + wms_options = {'format': 'image/jpeg'} debug = False widget = OpenLayersWidget @@ -76,6 +77,12 @@ class GeoModelAdmin(ModelAdmin): class OLMap(self.widget): template = self.map_template geom_type = db_field.geom_type + + wms_options = '' + if self.wms_options: + wms_options = ["%s: '%s'" % pair for pair in self.wms_options.items()] + wms_options = ', %s' % ', '.join(wms_options) + params = {'default_lon' : self.default_lon, 'default_lat' : self.default_lat, 'default_zoom' : self.default_zoom, @@ -106,6 +113,7 @@ class GeoModelAdmin(ModelAdmin): 'wms_url' : self.wms_url, 'wms_layer' : self.wms_layer, 'wms_name' : self.wms_name, + 'wms_options' : wms_options, 'debug' : self.debug, } return OLMap diff --git a/django/contrib/gis/gdal/base.py b/django/contrib/gis/gdal/base.py index f9455c783a..36c03eb51e 100644 --- a/django/contrib/gis/gdal/base.py +++ b/django/contrib/gis/gdal/base.py @@ -1,5 +1,5 @@ from ctypes import c_void_p -from types import NoneType + from django.contrib.gis.gdal.error import GDALException class GDALBase(object): @@ -26,7 +26,7 @@ class GDALBase(object): # compatible type or None (NULL). if isinstance(ptr, (int, long)): self._ptr = self.ptr_type(ptr) - elif isinstance(ptr, (self.ptr_type, NoneType)): + elif ptr is None or isinstance(ptr, self.ptr_type): self._ptr = ptr else: raise TypeError('Incompatible pointer type') diff --git a/django/contrib/gis/gdal/tests/__init__.py b/django/contrib/gis/gdal/tests/__init__.py index ee9fad2a67..eb72a38775 100644 --- a/django/contrib/gis/gdal/tests/__init__.py +++ b/django/contrib/gis/gdal/tests/__init__.py @@ -2,10 +2,12 @@ Module for executing all of the GDAL tests. None of these tests require the use of the database. """ +from __future__ import absolute_import + from django.utils.unittest import TestSuite, TextTestRunner # Importing the GDAL test modules. -import test_driver, test_ds, test_envelope, test_geom, test_srs +from . import test_driver, test_ds, test_envelope, test_geom, test_srs test_suites = [test_driver.suite(), test_ds.suite(), diff --git a/django/contrib/gis/geoip/__init__.py b/django/contrib/gis/geoip/__init__.py index 2e38e636f7..8b519a242d 100644 --- a/django/contrib/gis/geoip/__init__.py +++ b/django/contrib/gis/geoip/__init__.py @@ -11,8 +11,10 @@ Grab GeoIP.dat.gz and GeoLiteCity.dat.gz, and unzip them in the directory corresponding to settings.GEOIP_PATH. """ +from __future__ import absolute_import + try: - from django.contrib.gis.geoip.base import GeoIP, GeoIPException + from .base import GeoIP, GeoIPException HAS_GEOIP = True except: HAS_GEOIP = False diff --git a/django/contrib/gis/geos/base.py b/django/contrib/gis/geos/base.py index b3012dd930..754bcce7ad 100644 --- a/django/contrib/gis/geos/base.py +++ b/django/contrib/gis/geos/base.py @@ -1,5 +1,5 @@ from ctypes import c_void_p -from types import NoneType + from django.contrib.gis.geos.error import GEOSException # Trying to import GDAL libraries, if available. Have to place in @@ -41,7 +41,7 @@ class GEOSBase(object): def _set_ptr(self, ptr): # Only allow the pointer to be set with pointers of the # compatible type or None (NULL). - if isinstance(ptr, (self.ptr_type, NoneType)): + if ptr is None or isinstance(ptr, self.ptr_type): self._ptr = ptr else: raise TypeError('Incompatible pointer type') diff --git a/django/contrib/gis/geos/mutable_list.py b/django/contrib/gis/geos/mutable_list.py index cc28147661..1a9dcf0b5b 100644 --- a/django/contrib/gis/geos/mutable_list.py +++ b/django/contrib/gis/geos/mutable_list.py @@ -8,6 +8,9 @@ See also http://www.aryehleib.com/MutableLists.html Author: Aryeh Leib Taurog. """ +from django.utils.functional import total_ordering + +@total_ordering class ListMixin(object): """ A base class which provides complete list interface. @@ -143,20 +146,28 @@ class ListMixin(object): self.extend(cache) return self - def __cmp__(self, other): - 'cmp' + def __eq__(self, other): + for i in range(len(self)): + try: + c = self[i] == other[i] + except IndexError: + # must be other is shorter + return False + if not c: + return False + return True + + def __lt__(self, other): slen = len(self) for i in range(slen): try: - c = cmp(self[i], other[i]) + c = self[i] < other[i] except IndexError: # must be other is shorter - return 1 - else: - # elements not equal - if c: return c - - return cmp(slen, len(other)) + return False + if c: + return c + return slen < len(other) ### Public list interface Methods ### ## Non-mutating ## diff --git a/django/contrib/gis/geos/tests/__init__.py b/django/contrib/gis/geos/tests/__init__.py index 6c6c18e496..ccf960c68f 100644 --- a/django/contrib/gis/geos/tests/__init__.py +++ b/django/contrib/gis/geos/tests/__init__.py @@ -1,8 +1,10 @@ """ GEOS Testing module. """ +from __future__ import absolute_import + from django.utils.unittest import TestSuite, TextTestRunner -import test_geos, test_io, test_geos_mutation, test_mutable_list +from . import test_geos, test_io, test_geos_mutation, test_mutable_list test_suites = [ test_geos.suite(), diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 4a04d15ad9..f435cdaff1 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1,18 +1,23 @@ import ctypes import random -import unittest -from django.contrib.gis.geos import * + +from django.contrib.gis.geos import (GEOSException, GEOSIndexError, GEOSGeometry, + GeometryCollection, Point, MultiPoint, Polygon, MultiPolygon, LinearRing, + LineString, MultiLineString, fromfile, fromstr, geos_version_info) from django.contrib.gis.geos.base import gdal, numpy, GEOSBase from django.contrib.gis.geos.libgeos import GEOS_PREPARE from django.contrib.gis.geometry.test_data import TestDataMixin +from django.utils import unittest + + class GEOSTest(unittest.TestCase, TestDataMixin): @property def null_srid(self): """ Returns the proper null SRID depending on the GEOS version. - See the comments in `test15_srid` for more details. + See the comments in `test_srid` for more details. """ info = geos_version_info() if info['version'] == '3.0.0' and info['release_candidate']: @@ -20,7 +25,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): else: return None - def test00_base(self): + def test_base(self): "Tests out the GEOSBase class." # Testing out GEOSBase class, which provides a `ptr` property # that abstracts out access to underlying C pointers. @@ -62,19 +67,19 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertRaises(TypeError, fg1._set_ptr, bad_ptr) self.assertRaises(TypeError, fg2._set_ptr, bad_ptr) - def test01a_wkt(self): + def test_wkt(self): "Testing WKT output." for g in self.geometries.wkt_out: geom = fromstr(g.wkt) self.assertEqual(g.ewkt, geom.wkt) - def test01b_hex(self): + def test_hex(self): "Testing HEX output." for g in self.geometries.hex_wkt: geom = fromstr(g.wkt) self.assertEqual(g.hex, geom.hex) - def test01b_hexewkb(self): + def test_hexewkb(self): "Testing (HEX)EWKB output." from binascii import a2b_hex @@ -124,14 +129,14 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # Redundant sanity check. self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid) - def test01c_kml(self): + def test_kml(self): "Testing KML output." for tg in self.geometries.wkt_out: geom = fromstr(tg.wkt) kml = getattr(tg, 'kml', False) if kml: self.assertEqual(kml, geom.kml) - def test01d_errors(self): + def test_errors(self): "Testing the Error handlers." # string-based print("\nBEGIN - expecting GEOS_ERROR; safe to ignore.\n") @@ -154,7 +159,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # None self.assertRaises(TypeError, GEOSGeometry, None) - def test01e_wkb(self): + def test_wkb(self): "Testing WKB output." from binascii import b2a_hex for g in self.geometries.hex_wkt: @@ -162,7 +167,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): wkb = geom.wkb self.assertEqual(b2a_hex(wkb).upper(), g.hex) - def test01f_create_hex(self): + def test_create_hex(self): "Testing creation from HEX." for g in self.geometries.hex_wkt: geom_h = GEOSGeometry(g.hex) @@ -170,7 +175,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): geom_t = fromstr(g.wkt) self.assertEqual(geom_t.wkt, geom_h.wkt) - def test01g_create_wkb(self): + def test_create_wkb(self): "Testing creation from WKB." from binascii import a2b_hex for g in self.geometries.hex_wkt: @@ -180,7 +185,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): geom_t = fromstr(g.wkt) self.assertEqual(geom_t.wkt, geom_h.wkt) - def test01h_ewkt(self): + def test_ewkt(self): "Testing EWKT." srids = (-1, 32140) for srid in srids: @@ -191,9 +196,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(srid, poly.shell.srid) self.assertEqual(srid, fromstr(poly.ewkt).srid) # Checking export - def test01i_json(self): + @unittest.skipUnless(gdal.HAS_GDAL and gdal.GEOJSON, "gdal >= 1.5 is required") + def test_json(self): "Testing GeoJSON input/output (via GDAL)." - if not gdal or not gdal.GEOJSON: return for g in self.geometries.json_geoms: geom = GEOSGeometry(g.wkt) if not hasattr(g, 'not_equal'): @@ -201,7 +206,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(g.json, geom.geojson) self.assertEqual(GEOSGeometry(g.wkt), GEOSGeometry(geom.json)) - def test01k_fromfile(self): + def test_fromfile(self): "Testing the fromfile() factory." from io import BytesIO ref_pnt = GEOSGeometry('POINT(5 23)') @@ -218,7 +223,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): pnt = fromfile(fh) self.assertEqual(ref_pnt, pnt) - def test01k_eq(self): + def test_eq(self): "Testing equivalence." p = fromstr('POINT(5 23)') self.assertEqual(p, p.wkt) @@ -233,7 +238,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertNotEqual(g, {'foo' : 'bar'}) self.assertNotEqual(g, False) - def test02a_points(self): + def test_points(self): "Testing Point objects." prev = fromstr('POINT(0 0)') for p in self.geometries.points: @@ -288,7 +293,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): prev = pnt # setting the previous geometry - def test02b_multipoints(self): + def test_multipoints(self): "Testing MultiPoint objects." for mp in self.geometries.multipoints: mpnt = fromstr(mp.wkt) @@ -307,7 +312,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(p.empty, False) self.assertEqual(p.valid, True) - def test03a_linestring(self): + def test_linestring(self): "Testing LineString objects." prev = fromstr('POINT(0 0)') for l in self.geometries.linestrings: @@ -333,7 +338,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(ls.wkt, LineString(*tuple(Point(tup) for tup in ls.tuple)).wkt) # Point individual arguments if numpy: self.assertEqual(ls, LineString(numpy.array(ls.tuple))) # as numpy array - def test03b_multilinestring(self): + def test_multilinestring(self): "Testing MultiLineString objects." prev = fromstr('POINT(0 0)') for l in self.geometries.multilinestrings: @@ -357,7 +362,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(ml.wkt, MultiLineString(*tuple(s.clone() for s in ml)).wkt) self.assertEqual(ml, MultiLineString(*tuple(LineString(s.tuple) for s in ml))) - def test04_linearring(self): + def test_linearring(self): "Testing LinearRing objects." for rr in self.geometries.linearrings: lr = fromstr(rr.wkt) @@ -373,14 +378,15 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(lr, LinearRing([list(tup) for tup in lr.tuple])) if numpy: self.assertEqual(lr, LinearRing(numpy.array(lr.tuple))) - def test05a_polygons(self): - "Testing Polygon objects." - - # Testing `from_bbox` class method + def test_polygons_from_bbox(self): + "Testing `from_bbox` class method." bbox = (-180, -90, 180, 90) - p = Polygon.from_bbox( bbox ) + p = Polygon.from_bbox(bbox) self.assertEqual(bbox, p.extent) + def test_polygons(self): + "Testing Polygon objects." + prev = fromstr('POINT(0 0)') for p in self.geometries.polygons: # Creating the Polygon, testing its properties. @@ -437,7 +443,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(poly.wkt, Polygon(*tuple(r for r in poly)).wkt) self.assertEqual(poly.wkt, Polygon(*tuple(LinearRing(r.tuple) for r in poly)).wkt) - def test05b_multipolygons(self): + def test_multipolygons(self): "Testing MultiPolygon objects." print("\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n") prev = fromstr('POINT (0 0)') @@ -460,7 +466,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n") - def test06a_memory_hijinks(self): + def test_memory_hijinks(self): "Testing Geometry __del__() on rings and polygons." #### Memory issues with rings and polygons @@ -483,7 +489,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # Access to these rings is OK since they are clones. s1, s2 = str(ring1), str(ring2) - def test08_coord_seq(self): + def test_coord_seq(self): "Testing Coordinate Sequence objects." for p in self.geometries.polygons: if p.ext_ring_cs: @@ -510,7 +516,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): cs[i] = tset self.assertEqual(tset[j], cs[i][j]) - def test09_relate_pattern(self): + def test_relate_pattern(self): "Testing relate() and relate_pattern()." g = fromstr('POINT (0 0)') self.assertRaises(GEOSException, g.relate_pattern, 0, 'invalid pattern, yo') @@ -520,7 +526,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(rg.result, a.relate_pattern(b, rg.pattern)) self.assertEqual(rg.pattern, a.relate(b)) - def test10_intersection(self): + def test_intersection(self): "Testing intersects() and intersection()." for i in xrange(len(self.geometries.topology_geoms)): a = fromstr(self.geometries.topology_geoms[i].wkt_a) @@ -533,7 +539,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): a &= b # testing __iand__ self.assertEqual(i1, a) - def test11_union(self): + def test_union(self): "Testing union()." for i in xrange(len(self.geometries.topology_geoms)): a = fromstr(self.geometries.topology_geoms[i].wkt_a) @@ -545,7 +551,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): a |= b # testing __ior__ self.assertEqual(u1, a) - def test12_difference(self): + def test_difference(self): "Testing difference()." for i in xrange(len(self.geometries.topology_geoms)): a = fromstr(self.geometries.topology_geoms[i].wkt_a) @@ -557,7 +563,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): a -= b # testing __isub__ self.assertEqual(d1, a) - def test13_symdifference(self): + def test_symdifference(self): "Testing sym_difference()." for i in xrange(len(self.geometries.topology_geoms)): a = fromstr(self.geometries.topology_geoms[i].wkt_a) @@ -569,7 +575,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): a ^= b # testing __ixor__ self.assertEqual(d1, a) - def test14_buffer(self): + def test_buffer(self): "Testing buffer()." for bg in self.geometries.buffer_geoms: g = fromstr(bg.wkt) @@ -597,7 +603,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertAlmostEqual(exp_ring[k][0], buf_ring[k][0], 9) self.assertAlmostEqual(exp_ring[k][1], buf_ring[k][1], 9) - def test15_srid(self): + def test_srid(self): "Testing the SRID property and keyword." # Testing SRID keyword on Point pnt = Point(5, 23, srid=4326) @@ -635,7 +641,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): p3 = fromstr(p1.hex, srid=-1) # -1 is intended. self.assertEqual(-1, p3.srid) - def test16_mutable_geometries(self): + def test_mutable_geometries(self): "Testing the mutability of Polygons and Geometry Collections." ### Testing the mutability of Polygons ### for p in self.geometries.polygons: @@ -699,7 +705,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): #self.assertEqual((3.14, 2.71), mpoly[0].shell[0]) #del mpoly - def test17_threed(self): + def test_threed(self): "Testing three-dimensional geometries." # Testing a 3D Point pnt = Point(2, 3, 8) @@ -715,7 +721,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): ls[0] = (1.,2.,3.) self.assertEqual((1.,2.,3.), ls[0]) - def test18_distance(self): + def test_distance(self): "Testing the distance() function." # Distance to self should be 0. pnt = Point(0, 0) @@ -733,7 +739,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): ls2 = LineString((5, 2), (6, 1), (7, 0)) self.assertEqual(3, ls1.distance(ls2)) - def test19_length(self): + def test_length(self): "Testing the length property." # Points have 0 length. pnt = Point(0, 0) @@ -751,7 +757,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): mpoly = MultiPolygon(poly.clone(), poly) self.assertEqual(8.0, mpoly.length) - def test20a_emptyCollections(self): + def test_emptyCollections(self): "Testing empty geometries and collections." gc1 = GeometryCollection([]) gc2 = fromstr('GEOMETRYCOLLECTION EMPTY') @@ -789,7 +795,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): else: self.assertRaises(GEOSIndexError, g.__getitem__, 0) - def test20b_collections_of_collections(self): + def test_collections_of_collections(self): "Testing GeometryCollection handling of other collections." # Creating a GeometryCollection WKT string composed of other # collections and polygons. @@ -808,9 +814,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): # And, they should be equal. self.assertEqual(gc1, gc2) - def test21_test_gdal(self): + @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required") + def test_gdal(self): "Testing `ogr` and `srs` properties." - if not gdal.HAS_GDAL: return g1 = fromstr('POINT(5 23)') self.assertEqual(True, isinstance(g1.ogr, gdal.OGRGeometry)) self.assertEqual(g1.srs, None) @@ -821,7 +827,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(g2.hex, g2.ogr.hex) self.assertEqual('WGS 84', g2.srs.name) - def test22_copy(self): + def test_copy(self): "Testing use with the Python `copy` module." import copy poly = GEOSGeometry('POLYGON((0 0, 0 23, 23 23, 23 0, 0 0), (5 5, 5 10, 10 10, 10 5, 5 5))') @@ -830,9 +836,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertNotEqual(poly._ptr, cpy1._ptr) self.assertNotEqual(poly._ptr, cpy2._ptr) - def test23_transform(self): + @unittest.skipUnless(gdal.HAS_GDAL, "gdal is required") + def test_transform(self): "Testing `transform` method." - if not gdal.HAS_GDAL: return orig = GEOSGeometry('POINT (-104.609 38.255)', 4326) trans = GEOSGeometry('POINT (992385.4472045 481455.4944650)', 2774) @@ -855,7 +861,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertAlmostEqual(trans.x, p.x, prec) self.assertAlmostEqual(trans.y, p.y, prec) - def test23_transform_noop(self): + def test_transform_noop(self): """ Testing `transform` method (SRID match) """ # transform() should no-op if source & dest SRIDs match, # regardless of whether GDAL is available. @@ -890,7 +896,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): finally: gdal.HAS_GDAL = old_has_gdal - def test23_transform_nosrid(self): + def test_transform_nosrid(self): """ Testing `transform` method (no SRID or negative SRID) """ g = GEOSGeometry('POINT (-104.609 38.255)', srid=None) @@ -905,7 +911,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): g = GEOSGeometry('POINT (-104.609 38.255)', srid=-1) self.assertRaises(GEOSException, g.transform, 2774, clone=True) - def test23_transform_nogdal(self): + def test_transform_nogdal(self): """ Testing `transform` method (GDAL not available) """ old_has_gdal = gdal.HAS_GDAL try: @@ -919,7 +925,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): finally: gdal.HAS_GDAL = old_has_gdal - def test24_extent(self): + def test_extent(self): "Testing `extent` method." # The xmin, ymin, xmax, ymax of the MultiPoint should be returned. mp = MultiPoint(Point(5, 23), Point(0, 0), Point(10, 50)) @@ -935,7 +941,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): xmax, ymax = max(x), max(y) self.assertEqual((xmin, ymin, xmax, ymax), poly.extent) - def test25_pickle(self): + def test_pickle(self): "Testing pickling and unpickling support." # Using both pickle and cPickle -- just 'cause. import pickle, cPickle @@ -958,9 +964,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(geom, tmpg) if not no_srid: self.assertEqual(geom.srid, tmpg.srid) - def test26_prepared(self): + @unittest.skipUnless(GEOS_PREPARE, "geos >= 3.1.0 is required") + def test_prepared(self): "Testing PreparedGeometry support." - if not GEOS_PREPARE: return # Creating a simple multipolygon and getting a prepared version. mpoly = GEOSGeometry('MULTIPOLYGON(((0 0,0 5,5 5,5 0,0 0)),((5 5,5 10,10 10,10 5,5 5)))') prep = mpoly.prepared @@ -974,7 +980,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): self.assertEqual(mpoly.intersects(pnt), prep.intersects(pnt)) self.assertEqual(c, prep.covers(pnt)) - def test26_line_merge(self): + def test_line_merge(self): "Testing line merge support" ref_geoms = (fromstr('LINESTRING(1 1, 1 1, 3 3)'), fromstr('MULTILINESTRING((1 1, 3 3), (3 3, 4 2))'), @@ -985,10 +991,9 @@ class GEOSTest(unittest.TestCase, TestDataMixin): for geom, merged in zip(ref_geoms, ref_merged): self.assertEqual(merged, geom.merged) - def test27_valid_reason(self): + @unittest.skipUnless(GEOS_PREPARE, "geos >= 3.1.0 is required") + def test_valid_reason(self): "Testing IsValidReason support" - # Skipping tests if GEOS < v3.1. - if not GEOS_PREPARE: return g = GEOSGeometry("POINT(0 0)") self.assertTrue(g.valid) @@ -1005,7 +1010,7 @@ class GEOSTest(unittest.TestCase, TestDataMixin): print("\nEND - expecting GEOS_NOTICE; safe to ignore.\n") - def test28_geos_version(self): + def test_geos_version(self): "Testing the GEOS version regular expression." from django.contrib.gis.geos.libgeos import version_regex versions = [ ('3.0.0rc4-CAPI-1.3.3', '3.0.0'), diff --git a/django/contrib/gis/maps/google/overlays.py b/django/contrib/gis/maps/google/overlays.py index c2ebb3c992..eaf12dad6d 100644 --- a/django/contrib/gis/maps/google/overlays.py +++ b/django/contrib/gis/maps/google/overlays.py @@ -1,5 +1,7 @@ -from django.utils.safestring import mark_safe from django.contrib.gis.geos import fromstr, Point, LineString, LinearRing, Polygon +from django.utils.functional import total_ordering +from django.utils.safestring import mark_safe + class GEvent(object): """ @@ -166,6 +168,7 @@ class GPolyline(GOverlayBase): return '%s, "%s", %s, %s' % (self.latlngs, self.color, self.weight, self.opacity) +@total_ordering class GIcon(object): """ Creates a GIcon object to pass into a Gmarker object. @@ -231,8 +234,11 @@ class GIcon(object): self.iconanchor = iconanchor self.infowindowanchor = infowindowanchor - def __cmp__(self, other): - return cmp(self.varname, other.varname) + def __eq__(self, other): + return self.varname == other.varname + + def __lt__(self, other): + return self.varname < other.varname def __hash__(self): # XOR with hash of GIcon type so that hash('varname') won't diff --git a/django/contrib/gis/measure.py b/django/contrib/gis/measure.py index a60398bccd..46e198c410 100644 --- a/django/contrib/gis/measure.py +++ b/django/contrib/gis/measure.py @@ -38,6 +38,8 @@ and Geoff Biggs' PhD work on dimensioned units for robotics. __all__ = ['A', 'Area', 'D', 'Distance'] from decimal import Decimal +from django.utils.functional import total_ordering + class MeasureBase(object): def default_units(self, kwargs): """ @@ -84,6 +86,7 @@ class MeasureBase(object): else: raise Exception('Could not find a unit keyword associated with "%s"' % unit_str) +@total_ordering class Distance(MeasureBase): UNITS = { 'chain' : 20.1168, @@ -178,9 +181,15 @@ class Distance(MeasureBase): def __str__(self): return '%s %s' % (getattr(self, self._default_unit), self._default_unit) - def __cmp__(self, other): + def __eq__(self, other): if isinstance(other, Distance): - return cmp(self.m, other.m) + return self.m == other.m + else: + return NotImplemented + + def __lt__(self, other): + if isinstance(other, Distance): + return self.m < other.m else: return NotImplemented @@ -244,6 +253,7 @@ class Distance(MeasureBase): def __nonzero__(self): return bool(self.m) +@total_ordering class Area(MeasureBase): # Getting the square units values and the alias dictionary. UNITS = dict([('sq_%s' % k, v ** 2) for k, v in Distance.UNITS.items()]) @@ -267,9 +277,15 @@ class Area(MeasureBase): def __str__(self): return '%s %s' % (getattr(self, self._default_unit), self._default_unit) - def __cmp__(self, other): + def __eq__(self, other): if isinstance(other, Area): - return cmp(self.sq_m, other.sq_m) + return self.sq_m == other.sq_m + else: + return NotImplemented + + def __lt__(self, other): + if isinstance(other, Area): + return self.sq_m < other.sq_m else: return NotImplemented diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js index adca203969..f54b75e258 100644 --- a/django/contrib/gis/templates/gis/admin/openlayers.js +++ b/django/contrib/gis/templates/gis/admin/openlayers.js @@ -111,7 +111,7 @@ OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.Sp // The admin map for this geometry field. {{ module }}.map = new OpenLayers.Map('{{ id }}_map', options); // Base Layer - {{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS( "{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'} );{% endblock %} + {{ module }}.layers.base = {% block base_layer %}new OpenLayers.Layer.WMS("{{ wms_name }}", "{{ wms_url }}", {layers: '{{ wms_layer }}'{{ wms_options|safe }}});{% endblock %} {{ module }}.map.addLayer({{ module }}.layers.base); {% block extra_layers %}{% endblock %} {% if is_linestring %}OpenLayers.Feature.Vector.style["default"]["strokeWidth"] = 3; // Default too thin for linestrings. {% endif %} diff --git a/django/contrib/gis/tests/geoadmin/tests.py b/django/contrib/gis/tests/geoadmin/tests.py index aa07c2c500..53c00c6588 100644 --- a/django/contrib/gis/tests/geoadmin/tests.py +++ b/django/contrib/gis/tests/geoadmin/tests.py @@ -2,6 +2,7 @@ from __future__ import absolute_import from django.test import TestCase from django.contrib.gis import admin +from django.contrib.gis.geos import Point from .models import City @@ -14,3 +15,21 @@ class GeoAdminTest(TestCase): admin_js = geoadmin.media.render_js() self.assertTrue(any([geoadmin.openlayers_url in js for js in admin_js])) + def test_olmap_OSM_rendering(self): + geoadmin = admin.site._registry[City] + result = geoadmin.get_map_widget(City._meta.get_field('point'))( + ).render('point', Point(-79.460734, 40.18476)) + self.assertIn( + """geodjango_point.layers.base = new OpenLayers.Layer.OSM("OpenStreetMap (Mapnik)");""", + result) + + def test_olmap_WMS_rendering(self): + admin.site.unregister(City) + admin.site.register(City, admin.GeoModelAdmin) + + geoadmin = admin.site._registry[City] + result = geoadmin.get_map_widget(City._meta.get_field('point'))( + ).render('point', Point(-79.460734, 40.18476)) + self.assertIn( + """geodjango_point.layers.base = new OpenLayers.Layer.WMS("OpenLayers WMS", "http://vmap0.tiles.osgeo.org/wms/vmap0", {layers: \'basic\', format: 'image/jpeg'});""", + result) diff --git a/django/contrib/gis/tests/layermap/models.py b/django/contrib/gis/tests/layermap/models.py index 51213eb0b8..b58dccdf37 100644 --- a/django/contrib/gis/tests/layermap/models.py +++ b/django/contrib/gis/tests/layermap/models.py @@ -17,6 +17,7 @@ class CountyFeat(models.Model): class City(models.Model): name = models.CharField(max_length=25) + name_txt = models.TextField(default='') population = models.IntegerField() density = models.DecimalField(max_digits=7, decimal_places=1) dt = models.DateField() diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index ef0e15e154..85b8d0c8b5 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -4,11 +4,11 @@ import os from copy import copy from decimal import Decimal -from django.utils.unittest import TestCase - from django.contrib.gis.gdal import DataSource from django.contrib.gis.tests.utils import mysql -from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey +from django.contrib.gis.utils.layermapping import (LayerMapping, LayerMapError, + InvalidDecimal, MissingForeignKey) +from django.test import TestCase from .models import ( City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State, @@ -28,7 +28,7 @@ STATES = ['Texas', 'Texas', 'Texas', 'Hawaii', 'Colorado'] class LayerMapTest(TestCase): - def test01_init(self): + def test_init(self): "Testing LayerMapping initialization." # Model field that does not exist. @@ -46,22 +46,14 @@ class LayerMapTest(TestCase): # Incrementing through the bad mapping dictionaries and # ensuring that a LayerMapError is raised. for bad_map in (bad1, bad2, bad3): - try: + with self.assertRaises(LayerMapError): lm = LayerMapping(City, city_shp, bad_map) - except LayerMapError: - pass - else: - self.fail('Expected a LayerMapError.') # A LookupError should be thrown for bogus encodings. - try: + with self.assertRaises(LookupError): lm = LayerMapping(City, city_shp, city_mapping, encoding='foobar') - except LookupError: - pass - else: - self.fail('Expected a LookupError') - def test02_simple_layermap(self): + def test_simple_layermap(self): "Test LayerMapping import of a simple point shapefile." # Setting up for the LayerMapping. lm = LayerMapping(City, city_shp, city_mapping) @@ -85,18 +77,14 @@ class LayerMapTest(TestCase): self.assertAlmostEqual(pnt1.x, pnt2.x, 5) self.assertAlmostEqual(pnt1.y, pnt2.y, 5) - def test03_layermap_strict(self): + def test_layermap_strict(self): "Testing the `strict` keyword, and import of a LineString shapefile." # When the `strict` keyword is set an error encountered will force # the importation to stop. - try: + with self.assertRaises(InvalidDecimal): lm = LayerMapping(Interstate, inter_shp, inter_mapping) lm.save(silent=True, strict=True) - except InvalidDecimal: - # No transactions for geoms on MySQL; delete added features. - if mysql: Interstate.objects.all().delete() - else: - self.fail('Should have failed on strict import with invalid decimal values.') + Interstate.objects.all().delete() # This LayerMapping should work b/c `strict` is not set. lm = LayerMapping(Interstate, inter_shp, inter_mapping) @@ -137,7 +125,7 @@ class LayerMapTest(TestCase): qs = CountyFeat.objects.filter(name=name) self.assertEqual(n, qs.count()) - def test04_layermap_unique_multigeometry_fk(self): + def test_layermap_unique_multigeometry_fk(self): "Testing the `unique`, and `transform`, geometry collection conversion, and ForeignKey mappings." # All the following should work. try: @@ -176,8 +164,9 @@ class LayerMapTest(TestCase): self.assertRaises(MissingForeignKey, lm.save, silent=True, strict=True) # Now creating the state models so the ForeignKey mapping may work. - co, hi, tx = State(name='Colorado'), State(name='Hawaii'), State(name='Texas') - co.save(), hi.save(), tx.save() + State.objects.bulk_create([ + State(name='Colorado'), State(name='Hawaii'), State(name='Texas') + ]) # If a mapping is specified as a collection, all OGR fields that # are not collections will be converted into them. For example, @@ -203,16 +192,19 @@ class LayerMapTest(TestCase): # The county helper is called to ensure integrity of County models. self.county_helper() - def test05_test_fid_range_step(self): + def test_test_fid_range_step(self): "Tests the `fid_range` keyword and the `step` keyword of .save()." # Function for clearing out all the counties before testing. def clear_counties(): County.objects.all().delete() + State.objects.bulk_create([ + State(name='Colorado'), State(name='Hawaii'), State(name='Texas') + ]) + # Initializing the LayerMapping object to use in these tests. lm = LayerMapping(County, co_shp, co_mapping, transform=False, unique='name') # Bad feature id ranges should raise a type error. - clear_counties() bad_ranges = (5.0, 'foo', co_shp) for bad in bad_ranges: self.assertRaises(TypeError, lm.save, fid_range=bad) @@ -241,8 +233,10 @@ class LayerMapTest(TestCase): self.assertEqual(2, qs.count()) hi, co = tuple(qs) hi_idx, co_idx = tuple(map(NAMES.index, ('Honolulu', 'Pueblo'))) - self.assertEqual('Pueblo', co.name); self.assertEqual(NUMS[co_idx], len(co.mpoly)) - self.assertEqual('Honolulu', hi.name); self.assertEqual(NUMS[hi_idx], len(hi.mpoly)) + self.assertEqual('Pueblo', co.name) + self.assertEqual(NUMS[co_idx], len(co.mpoly)) + self.assertEqual('Honolulu', hi.name) + self.assertEqual(NUMS[hi_idx], len(hi.mpoly)) # Testing the `step` keyword -- should get the same counties # regardless of we use a step that divides equally, that is odd, @@ -252,7 +246,7 @@ class LayerMapTest(TestCase): lm.save(step=st, strict=True) self.county_helper(county_feat=False) - def test06_model_inheritance(self): + def test_model_inheritance(self): "Tests LayerMapping on inherited models. See #12093." icity_mapping = {'name' : 'Name', 'population' : 'Population', @@ -272,9 +266,18 @@ class LayerMapTest(TestCase): self.assertEqual(6, ICity1.objects.count()) self.assertEqual(3, ICity2.objects.count()) - def test07_invalid_layer(self): + def test_invalid_layer(self): "Tests LayerMapping on invalid geometries. See #15378." invalid_mapping = {'point': 'POINT'} lm = LayerMapping(Invalid, invalid_shp, invalid_mapping, source_srs=4326) lm.save(silent=True) + + def test_textfield(self): + "Tests that String content fits also in a TextField" + mapping = copy(city_mapping) + mapping['name_txt'] = 'Name' + lm = LayerMapping(City, city_shp, mapping) + lm.save(silent=True, strict=True) + self.assertEqual(City.objects.count(), 3) + self.assertEqual(City.objects.all().order_by('name_txt')[0].name_txt, "Houston") diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index ea3f3d7861..48d6c1b70e 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -332,7 +332,7 @@ class LayerMapping(object): val = unicode(ogr_field.value, self.encoding) else: val = ogr_field.value - if len(val) > model_field.max_length: + if model_field.max_length and len(val) > model_field.max_length: raise InvalidString('%s model field maximum string length is %s, given %s characters.' % (model_field.name, model_field.max_length, len(val))) elif isinstance(ogr_field, OFTReal) and isinstance(model_field, models.DecimalField): diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py index 2aa36443b1..bf6684766b 100644 --- a/django/contrib/humanize/tests.py +++ b/django/contrib/humanize/tests.py @@ -190,7 +190,7 @@ class HumanizeTests(TestCase): orig_humanize_datetime = humanize.datetime orig_timesince_datetime = timesince.datetime humanize.datetime = MockDateTime - timesince.datetime = new.module("mock_datetime") + timesince.datetime = new.module(b"mock_datetime") timesince.datetime.datetime = MockDateTime try: diff --git a/django/contrib/localflavor/es/es_provinces.py b/django/contrib/localflavor/es/es_provinces.py index 9f5e12680b..51f681f4fc 100644 --- a/django/contrib/localflavor/es/es_provinces.py +++ b/django/contrib/localflavor/es/es_provinces.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ PROVINCE_CHOICES = ( - ('01', _('Arava')), + ('01', _('Araba')), ('02', _('Albacete')), ('03', _('Alacant')), ('04', _('Almeria')), diff --git a/django/contrib/messages/tests/base.py b/django/contrib/messages/tests/base.py index 4a00324a3d..1f64c61ecb 100644 --- a/django/contrib/messages/tests/base.py +++ b/django/contrib/messages/tests/base.py @@ -1,5 +1,5 @@ from django import http -from django.conf import settings +from django.conf import settings, global_settings from django.contrib.messages import constants, utils, get_level, set_level from django.contrib.messages.api import MessageFailure from django.contrib.messages.storage import default_storage, base @@ -57,6 +57,7 @@ class BaseTest(TestCase): def setUp(self): self.settings_override = override_settings_tags( TEMPLATE_DIRS = (), + TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS, MESSAGE_TAGS = '', MESSAGE_STORAGE = '%s.%s' % (self.storage_class.__module__, self.storage_class.__name__), diff --git a/django/contrib/sitemaps/tests/base.py b/django/contrib/sitemaps/tests/base.py index f1d6753b3a..224277e2a0 100644 --- a/django/contrib/sitemaps/tests/base.py +++ b/django/contrib/sitemaps/tests/base.py @@ -1,6 +1,3 @@ -import os - -from django.conf import settings from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.test import TestCase @@ -13,16 +10,9 @@ class SitemapTestsBase(TestCase): def setUp(self): self.base_url = '%s://%s' % (self.protocol, self.domain) - self.old_USE_L10N = settings.USE_L10N - self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS - settings.TEMPLATE_DIRS = ( - os.path.join(os.path.dirname(__file__), 'templates'), - ) self.old_Site_meta_installed = Site._meta.installed # Create a user that will double as sitemap content User.objects.create_user('testuser', 'test@example.com', 's3krit') def tearDown(self): - settings.USE_L10N = self.old_USE_L10N - settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS Site._meta.installed = self.old_Site_meta_installed diff --git a/django/contrib/sitemaps/tests/http.py b/django/contrib/sitemaps/tests/http.py index 3b56aa8d53..5786aa48d5 100644 --- a/django/contrib/sitemaps/tests/http.py +++ b/django/contrib/sitemaps/tests/http.py @@ -1,15 +1,19 @@ +import os from datetime import date + from django.conf import settings from django.contrib.auth.models import User from django.contrib.sitemaps import Sitemap, GenericSitemap from django.contrib.sites.models import Site from django.core.exceptions import ImproperlyConfigured +from django.test.utils import override_settings from django.utils.unittest import skipUnless from django.utils.formats import localize from django.utils.translation import activate, deactivate from .base import SitemapTestsBase + class HTTPSitemapTests(SitemapTestsBase): def test_simple_sitemap_index(self): @@ -21,6 +25,9 @@ class HTTPSitemapTests(SitemapTestsBase): """ % self.base_url) + @override_settings( + TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) + ) def test_simple_sitemap_custom_index(self): "A simple sitemap index can be rendered with a custom template" response = self.client.get('/simple/custom-index.xml') @@ -49,6 +56,9 @@ class HTTPSitemapTests(SitemapTestsBase): """ % (self.base_url, date.today())) + @override_settings( + TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) + ) def test_simple_custom_sitemap(self): "A simple sitemap can be rendered with a custom template" response = self.client.get('/simple/custom-sitemap.xml') @@ -60,10 +70,9 @@ class HTTPSitemapTests(SitemapTestsBase): """ % (self.base_url, date.today())) @skipUnless(settings.USE_I18N, "Internationalization is not enabled") + @override_settings(USE_L10N=True) def test_localized_priority(self): "The priority value should not be localized (Refs #14164)" - # Localization should be active - settings.USE_L10N = True activate('fr') self.assertEqual(u'0,3', localize(0.3)) diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index cab96c8d44..669c04043b 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -4,7 +4,7 @@ from optparse import make_option from django.core.files.storage import FileSystemStorage from django.core.management.base import CommandError, NoArgsCommand -from django.utils.encoding import smart_str, smart_unicode +from django.utils.encoding import smart_unicode from django.utils.datastructures import SortedDict from django.contrib.staticfiles import finders, storage @@ -178,15 +178,12 @@ Type 'yes' to continue, or 'no' to cancel: """ ', %s post-processed' % post_processed_count or ''), } - self.stdout.write(smart_str(summary)) + self.stdout.write(summary) def log(self, msg, level=2): """ Small log helper """ - msg = smart_str(msg) - if not msg.endswith("\n"): - msg += "\n" if self.verbosity >= level: self.stdout.write(msg) diff --git a/django/contrib/staticfiles/management/commands/findstatic.py b/django/contrib/staticfiles/management/commands/findstatic.py index bcf0c2f055..dc4406e458 100644 --- a/django/contrib/staticfiles/management/commands/findstatic.py +++ b/django/contrib/staticfiles/management/commands/findstatic.py @@ -23,9 +23,7 @@ class Command(LabelCommand): result = [result] output = u'\n '.join( (smart_unicode(os.path.realpath(path)) for path in result)) - self.stdout.write( - smart_str(u"Found '%s' here:\n %s\n" % (path, output))) + self.stdout.write(u"Found '%s' here:\n %s" % (path, output)) else: if verbosity >= 1: - self.stderr.write( - smart_str("No matching file found for '%s'.\n" % path)) + self.stderr.write("No matching file found for '%s'." % path) diff --git a/django/contrib/staticfiles/storage.py b/django/contrib/staticfiles/storage.py index 7b5866a699..fd8f9efb02 100644 --- a/django/contrib/staticfiles/storage.py +++ b/django/contrib/staticfiles/storage.py @@ -64,6 +64,17 @@ class CachedFilesMixin(object): compiled = re.compile(pattern) self._patterns.setdefault(extension, []).append(compiled) + def file_hash(self, name, content=None): + """ + Retuns a hash of the file with the given name and optional content. + """ + if content is None: + return None + md5 = hashlib.md5() + for chunk in content.chunks(): + md5.update(chunk) + return md5.hexdigest()[:12] + def hashed_name(self, name, content=None): parsed_name = urlsplit(unquote(name)) clean_name = parsed_name.path.strip() @@ -78,13 +89,11 @@ class CachedFilesMixin(object): return name path, filename = os.path.split(clean_name) root, ext = os.path.splitext(filename) - # Get the MD5 hash of the file - md5 = hashlib.md5() - for chunk in content.chunks(): - md5.update(chunk) - md5sum = md5.hexdigest()[:12] - hashed_name = os.path.join(path, u"%s.%s%s" % - (root, md5sum, ext)) + file_hash = self.file_hash(clean_name, content) + if file_hash is not None: + file_hash = u".%s" % file_hash + hashed_name = os.path.join(path, u"%s%s%s" % + (root, file_hash, ext)) unparsed_name = list(parsed_name) unparsed_name[2] = hashed_name # Special casing for a @font-face hack, like url(myfont.eot?#iefix") diff --git a/django/core/context_processors.py b/django/core/context_processors.py index 1a2a188246..3ba519188b 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -22,7 +22,7 @@ def csrf(request): # In order to be able to provide debugging info in the # case of misconfiguration, we use a sentinel value # instead of returning an empty dict. - return 'NOTPROVIDED' + return b'NOTPROVIDED' else: return token _get_val = lazy(_get_val, str) diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 201a854a4b..e3d1dc9c7e 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -1,6 +1,7 @@ """ Global Django exception and warning classes. """ +from functools import reduce class DjangoRuntimeWarning(RuntimeWarning): pass diff --git a/django/core/files/base.py b/django/core/files/base.py index 4d19acd5ba..a2d703c963 100644 --- a/django/core/files/base.py +++ b/django/core/files/base.py @@ -12,7 +12,8 @@ class File(FileProxyMixin): if name is None: name = getattr(file, 'name', None) self.name = name - self.mode = getattr(file, 'mode', None) + if hasattr(file, 'mode'): + self.mode = file.mode def __str__(self): return smart_str(self.name or '') @@ -125,7 +126,7 @@ class ContentFile(File): A File-like object that takes just raw content, rather than an actual file. """ def __init__(self, content, name=None): - content = content or '' + content = content or b'' super(ContentFile, self).__init__(BytesIO(content), name=name) self.size = len(content) diff --git a/django/core/files/uploadedfile.py b/django/core/files/uploadedfile.py index b58349e27b..97d53482e4 100644 --- a/django/core/files/uploadedfile.py +++ b/django/core/files/uploadedfile.py @@ -30,8 +30,8 @@ class UploadedFile(File): self.charset = charset def __repr__(self): - return "<%s: %s (%s)>" % ( - self.__class__.__name__, smart_str(self.name), self.content_type) + return smart_str("<%s: %s (%s)>" % ( + self.__class__.__name__, self.name, self.content_type)) def _get_name(self): return self._name diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 0639ef19da..128ff7ac64 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -78,14 +78,14 @@ class LimitedStream(object): def __init__(self, stream, limit, buf_size=64 * 1024 * 1024): self.stream = stream self.remaining = limit - self.buffer = '' + self.buffer = b'' self.buf_size = buf_size def _read_limited(self, size=None): if size is None or size > self.remaining: size = self.remaining if size == 0: - return '' + return b'' result = self.stream.read(size) self.remaining -= len(result) return result @@ -93,17 +93,17 @@ class LimitedStream(object): def read(self, size=None): if size is None: result = self.buffer + self._read_limited() - self.buffer = '' + self.buffer = b'' elif size < len(self.buffer): result = self.buffer[:size] self.buffer = self.buffer[size:] else: # size >= len(self.buffer) result = self.buffer + self._read_limited(size - len(self.buffer)) - self.buffer = '' + self.buffer = b'' return result def readline(self, size=None): - while '\n' not in self.buffer and \ + while b'\n' not in self.buffer and \ (size is None or len(self.buffer) < size): if size: # since size is not None here, len(self.buffer) < size diff --git a/django/core/management/base.py b/django/core/management/base.py index 121a5c6ab7..a204f6f0bc 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -45,6 +45,29 @@ def handle_default_options(options): sys.path.insert(0, options.pythonpath) +class OutputWrapper(object): + """ + Wrapper around stdout/stderr + """ + def __init__(self, out, style_func=None, ending='\n'): + self._out = out + self.style_func = None + if hasattr(out, 'isatty') and out.isatty(): + self.style_func = style_func + self.ending = ending + + def __getattr__(self, name): + return getattr(self._out, name) + + def write(self, msg, style_func=None, ending=None): + ending = ending is None and self.ending or ending + if ending and not msg.endswith(ending): + msg += ending + style_func = [f for f in (style_func, self.style_func, lambda x:x) + if f is not None][0] + self._out.write(smart_str(style_func(msg))) + + class BaseCommand(object): """ The base class from which all management commands ultimately @@ -74,8 +97,9 @@ class BaseCommand(object): output and, if the command is intended to produce a block of SQL statements, will be wrapped in ``BEGIN`` and ``COMMIT``. - 4. If ``handle()`` raised a ``CommandError``, ``execute()`` will - instead print an error message to ``stderr``. + 4. If ``handle()`` or ``execute()`` raised any exception (e.g. + ``CommandError``), ``run_from_argv()`` will instead print an error + message to ``stderr``. Thus, the ``handle()`` method is typically the starting point for subclasses; many built-in commands and command types either place @@ -187,46 +211,42 @@ class BaseCommand(object): def run_from_argv(self, argv): """ Set up any environment changes requested (e.g., Python path - and Django settings), then run this command. - + and Django settings), then run this command. If the + command raises a ``CommandError``, intercept it and print it sensibly + to stderr. """ parser = self.create_parser(argv[0], argv[1]) options, args = parser.parse_args(argv[2:]) handle_default_options(options) - self.execute(*args, **options.__dict__) + try: + self.execute(*args, **options.__dict__) + except Exception as e: + if options.traceback: + self.stderr.write(traceback.format_exc()) + self.stderr.write('%s: %s' % (e.__class__.__name__, e)) + sys.exit(1) def execute(self, *args, **options): """ Try to execute this command, performing model validation if needed (as controlled by the attribute - ``self.requires_model_validation``, except if force-skipped). If the - command raises a ``CommandError``, intercept it and print it sensibly - to stderr. + ``self.requires_model_validation``, except if force-skipped). """ - show_traceback = options.get('traceback', False) # Switch to English, because django-admin.py creates database content # like permissions, and those shouldn't contain any translations. # But only do this if we can assume we have a working settings file, # because django.utils.translation requires settings. saved_lang = None + self.stdout = OutputWrapper(options.get('stdout', sys.stdout)) + self.stderr = OutputWrapper(options.get('stderr', sys.stderr), self.style.ERROR) + if self.can_import_settings: - try: - from django.utils import translation - saved_lang = translation.get_language() - translation.activate('en-us') - except ImportError as e: - # If settings should be available, but aren't, - # raise the error and quit. - if show_traceback: - traceback.print_exc() - else: - sys.stderr.write(smart_str(self.style.ERROR('Error: %s\n' % e))) - sys.exit(1) + from django.utils import translation + saved_lang = translation.get_language() + translation.activate('en-us') try: - self.stdout = options.get('stdout', sys.stdout) - self.stderr = options.get('stderr', sys.stderr) if self.requires_model_validation and not options.get('skip_validation'): self.validate() output = self.handle(*args, **options) @@ -237,16 +257,10 @@ class BaseCommand(object): from django.db import connections, DEFAULT_DB_ALIAS connection = connections[options.get('database', DEFAULT_DB_ALIAS)] if connection.ops.start_transaction_sql(): - self.stdout.write(self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()) + '\n') + self.stdout.write(self.style.SQL_KEYWORD(connection.ops.start_transaction_sql())) self.stdout.write(output) if self.output_transaction: - self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;") + '\n') - except CommandError as e: - if show_traceback: - traceback.print_exc() - else: - self.stderr.write(smart_str(self.style.ERROR('Error: %s\n' % e))) - sys.exit(1) + self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;")) finally: if saved_lang is not None: translation.activate(saved_lang) @@ -266,7 +280,7 @@ class BaseCommand(object): error_text = s.read() raise CommandError("One or more models did not validate:\n%s" % error_text) if display_num_errors: - self.stdout.write("%s error%s found\n" % (num_errors, num_errors != 1 and 's' or '')) + self.stdout.write("%s error%s found" % (num_errors, num_errors != 1 and 's' or '')) def handle(self, *args, **options): """ diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index 354787eb91..fdc3535cf6 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -5,7 +5,7 @@ from optparse import make_option from django.core.management.base import BaseCommand, CommandError def has_bom(fn): - with open(fn, 'r') as f: + with open(fn, 'rb') as f: sample = f.read(4) return sample[:3] == '\xef\xbb\xbf' or \ sample.startswith(codecs.BOM_UTF16_LE) or \ diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index 82a126b0fa..bcc47e17c8 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -1,7 +1,7 @@ from optparse import make_option from django.core.cache.backends.db import BaseDatabaseCache -from django.core.management.base import LabelCommand +from django.core.management.base import LabelCommand, CommandError from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.db.utils import DatabaseError @@ -55,11 +55,10 @@ class Command(LabelCommand): try: curs.execute("\n".join(full_statement)) except DatabaseError as e: - self.stderr.write( - self.style.ERROR("Cache table '%s' could not be created.\nThe error was: %s.\n" % - (tablename, e))) transaction.rollback_unless_managed(using=db) - else: - for statement in index_output: - curs.execute(statement) - transaction.commit_unless_managed(using=db) + raise CommandError( + "Cache table '%s' could not be created.\nThe error was: %s." % + (tablename, e)) + for statement in index_output: + curs.execute(statement) + transaction.commit_unless_managed(using=db) diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index 71d6fa74f1..9059625dec 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -97,20 +97,24 @@ class Command(BaseCommand): except KeyError: raise CommandError("Unknown serialization format: %s" % format) - # Now collate the objects to be serialized. - objects = [] - for model in sort_dependencies(app_list.items()): - if model in excluded_models: - continue - if not model._meta.proxy and router.allow_syncdb(using, model): - if use_base_manager: - objects.extend(model._base_manager.using(using).all()) - else: - objects.extend(model._default_manager.using(using).all()) + def get_objects(): + # Collate the objects to be serialized. + for model in sort_dependencies(app_list.items()): + if model in excluded_models: + continue + if not model._meta.proxy and router.allow_syncdb(using, model): + if use_base_manager: + objects = model._base_manager + else: + objects = model._default_manager + for obj in objects.using(using).\ + order_by(model._meta.pk.name).iterator(): + yield obj try: - return serializers.serialize(format, objects, indent=indent, - use_natural_keys=use_natural_keys) + self.stdout.ending = None + serializers.serialize(format, get_objects(), indent=indent, + use_natural_keys=use_natural_keys, stream=self.stdout) except Exception as e: if show_traceback: raise diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index 10fd9bd463..a524e64f65 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -26,6 +26,8 @@ class Command(NoArgsCommand): def handle_inspection(self, options): connection = connections[options.get('database')] + # 'table_name_filter' is a stealth option + table_name_filter = options.get('table_name_filter') table2model = lambda table_name: table_name.title().replace('_', '').replace(' ', '').replace('-', '') @@ -43,6 +45,9 @@ class Command(NoArgsCommand): yield '' known_models = [] for table_name in connection.introspection.table_names(cursor): + if table_name_filter is not None and callable(table_name_filter): + if not table_name_filter(table_name): + continue yield 'class %s(models.Model):' % table2model(table_name) known_models.append(table2model(table_name)) try: diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 848b63705a..f44edf7ade 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -34,11 +34,11 @@ class Command(BaseCommand): using = options.get('database') connection = connections[using] - self.style = no_style() if not len(fixture_labels): self.stderr.write( - self.style.ERROR("No database fixture specified. Please provide the path of at least one fixture in the command line.\n") + "No database fixture specified. Please provide the path of at " + "least one fixture in the command line." ) return @@ -124,11 +124,11 @@ class Command(BaseCommand): if formats: if verbosity >= 2: - self.stdout.write("Loading '%s' fixtures...\n" % fixture_name) + self.stdout.write("Loading '%s' fixtures..." % fixture_name) else: self.stderr.write( - self.style.ERROR("Problem installing fixture '%s': %s is not a known serialization format.\n" % - (fixture_name, format))) + "Problem installing fixture '%s': %s is not a known serialization format." % + (fixture_name, format)) if commit: transaction.rollback(using=using) transaction.leave_transaction_management(using=using) @@ -141,7 +141,7 @@ class Command(BaseCommand): for fixture_dir in fixture_dirs: if verbosity >= 2: - self.stdout.write("Checking %s for fixtures...\n" % humanize(fixture_dir)) + self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir)) label_found = False for combo in product([using, None], formats, compression_formats): @@ -154,7 +154,7 @@ class Command(BaseCommand): ) if verbosity >= 3: - self.stdout.write("Trying %s for %s fixture '%s'...\n" % \ + self.stdout.write("Trying %s for %s fixture '%s'..." % \ (humanize(fixture_dir), file_name, fixture_name)) full_path = os.path.join(fixture_dir, file_name) open_method = compression_types[compression_format] @@ -162,13 +162,13 @@ class Command(BaseCommand): fixture = open_method(full_path, 'r') except IOError: if verbosity >= 2: - self.stdout.write("No %s fixture '%s' in %s.\n" % \ + self.stdout.write("No %s fixture '%s' in %s." % \ (format, fixture_name, humanize(fixture_dir))) else: try: if label_found: - self.stderr.write(self.style.ERROR("Multiple fixtures named '%s' in %s. Aborting.\n" % - (fixture_name, humanize(fixture_dir)))) + self.stderr.write("Multiple fixtures named '%s' in %s. Aborting." % + (fixture_name, humanize(fixture_dir))) if commit: transaction.rollback(using=using) transaction.leave_transaction_management(using=using) @@ -178,7 +178,7 @@ class Command(BaseCommand): objects_in_fixture = 0 loaded_objects_in_fixture = 0 if verbosity >= 2: - self.stdout.write("Installing %s fixture '%s' from %s.\n" % \ + self.stdout.write("Installing %s fixture '%s' from %s." % \ (format, fixture_name, humanize(fixture_dir))) objects = serializers.deserialize(format, fixture, using=using) @@ -209,8 +209,8 @@ class Command(BaseCommand): # error was encountered during fixture loading. if objects_in_fixture == 0: self.stderr.write( - self.style.ERROR("No fixture data found for '%s'. (File format may be invalid.)\n" % - (fixture_name))) + "No fixture data found for '%s'. (File format may be invalid.)" % + (fixture_name)) if commit: transaction.rollback(using=using) transaction.leave_transaction_management(using=using) @@ -231,16 +231,16 @@ class Command(BaseCommand): traceback.print_exc() else: self.stderr.write( - self.style.ERROR("Problem installing fixture '%s': %s\n" % + "Problem installing fixture '%s': %s" % (full_path, ''.join(traceback.format_exception(sys.exc_type, - sys.exc_value, sys.exc_traceback))))) + sys.exc_value, sys.exc_traceback)))) return # If we found even one object in a fixture, we need to reset the # database sequences. if loaded_object_count > 0: - sequence_sql = connection.ops.sequence_reset_sql(self.style, models) + sequence_sql = connection.ops.sequence_reset_sql(no_style(), models) if sequence_sql: if verbosity >= 2: self.stdout.write("Resetting sequences\n") @@ -253,10 +253,10 @@ class Command(BaseCommand): if verbosity >= 1: if fixture_object_count == loaded_object_count: - self.stdout.write("Installed %d object(s) from %d fixture(s)\n" % ( + self.stdout.write("Installed %d object(s) from %d fixture(s)" % ( loaded_object_count, fixture_count)) else: - self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)\n" % ( + self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" % ( loaded_object_count, fixture_object_count, fixture_count)) # Close the DB connection. This is required as a workaround for an diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index e6182198ec..c067c9c322 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -120,12 +120,12 @@ class Command(BaseCommand): error_text = ERRORS[e.args[0].args[0]] except (AttributeError, KeyError): error_text = str(e) - sys.stderr.write(self.style.ERROR("Error: %s" % error_text) + '\n') + sys.stderr.write("Error: %s" % error_text) # Need to use an OS exit because sys.exit doesn't work in a thread os._exit(1) except KeyboardInterrupt: if shutdown_message: - self.stdout.write("%s\n" % shutdown_message) + self.stdout.write(shutdown_message) sys.exit(0) diff --git a/django/core/management/templates.py b/django/core/management/templates.py index 735e29ad7f..623aa69deb 100644 --- a/django/core/management/templates.py +++ b/django/core/management/templates.py @@ -16,7 +16,6 @@ from os import path import django from django.template import Template, Context from django.utils import archive -from django.utils.encoding import smart_str from django.utils._os import rmtree_errorhandler from django.core.management.base import BaseCommand, CommandError from django.core.management.commands.makemessages import handle_extensions @@ -166,11 +165,10 @@ class TemplateCommand(BaseCommand): shutil.copymode(old_path, new_path) self.make_writeable(new_path) except OSError: - notice = self.style.NOTICE( + self.stderr.write( "Notice: Couldn't set permission bits on %s. You're " "probably using an uncommon filesystem setup. No " - "problem.\n" % new_path) - sys.stderr.write(smart_str(notice)) + "problem." % new_path, self.style.NOTICE) if self.paths_to_remove: if self.verbosity >= 2: diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 2e6a0656b1..04053c1f8f 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -39,6 +39,7 @@ class Serializer(object): self.use_natural_keys = options.pop("use_natural_keys", False) self.start_serialization() + self.first = True for obj in queryset: self.start_object(obj) # Use the concrete parent class' _meta instead of the object's _meta @@ -57,6 +58,8 @@ class Serializer(object): if self.selected_fields is None or field.attname in self.selected_fields: self.handle_m2m_field(obj, field) self.end_object(obj) + if self.first: + self.first = False self.end_serialization() return self.getvalue() diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py index 4da8581f68..fce00600f4 100644 --- a/django/core/serializers/json.py +++ b/django/core/serializers/json.py @@ -21,13 +21,38 @@ class Serializer(PythonSerializer): """ internal_use_only = False - def end_serialization(self): + def start_serialization(self): if json.__version__.split('.') >= ['2', '1', '3']: # Use JS strings to represent Python Decimal instances (ticket #16850) self.options.update({'use_decimal': False}) - json.dump(self.objects, self.stream, cls=DjangoJSONEncoder, **self.options) + self._current = None + self.json_kwargs = self.options.copy() + self.json_kwargs.pop('stream', None) + self.json_kwargs.pop('fields', None) + self.stream.write("[") + + def end_serialization(self): + if self.options.get("indent"): + self.stream.write("\n") + self.stream.write("]") + if self.options.get("indent"): + self.stream.write("\n") + + def end_object(self, obj): + # self._current has the field data + indent = self.options.get("indent") + if not self.first: + self.stream.write(",") + if not indent: + self.stream.write(" ") + if indent: + self.stream.write("\n") + json.dump(self.get_dump_object(obj), self.stream, + cls=DjangoJSONEncoder, **self.json_kwargs) + self._current = None def getvalue(self): + # overwrite PythonSerializer.getvalue() with base Serializer.getvalue() if callable(getattr(self.stream, 'getvalue', None)): return self.stream.getvalue() diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 195bf11d24..49120434eb 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -27,13 +27,16 @@ class Serializer(base.Serializer): self._current = {} def end_object(self, obj): - self.objects.append({ - "model" : smart_unicode(obj._meta), - "pk" : smart_unicode(obj._get_pk_val(), strings_only=True), - "fields" : self._current - }) + self.objects.append(self.get_dump_object(obj)) self._current = None + def get_dump_object(self, obj): + return { + "pk": smart_unicode(obj._get_pk_val(), strings_only=True), + "model": smart_unicode(obj._meta), + "fields": self._current + } + def handle_field(self, obj, field): value = field._get_val_from_obj(obj) # Protected types (i.e., primitives like None, numbers, dates, diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index 82c5aca149..be2962d660 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -146,25 +146,23 @@ class WSGIRequestHandler(simple_server.WSGIRequestHandler, object): env['PATH_INFO'] = urllib.unquote(path) env['QUERY_STRING'] = query env['REMOTE_ADDR'] = self.client_address[0] + env['CONTENT_TYPE'] = self.headers.get('content-type', 'text/plain') - if self.headers.typeheader is None: - env['CONTENT_TYPE'] = self.headers.type - else: - env['CONTENT_TYPE'] = self.headers.typeheader - - length = self.headers.getheader('content-length') + length = self.headers.get('content-length') if length: env['CONTENT_LENGTH'] = length - for h in self.headers.headers: - k,v = h.split(':',1) - k=k.replace('-','_').upper(); v=v.strip() - if k in env: - continue # skip content length, type,etc. - if 'HTTP_'+k in env: - env['HTTP_'+k] += ','+v # comma-separate multiple headers + for key, value in self.headers.items(): + key = key.replace('-','_').upper() + value = value.strip() + if key in env: + # Skip content length, type, etc. + continue + if 'HTTP_' + key in env: + # Comma-separate multiple headers + env['HTTP_' + key] += ',' + value else: - env['HTTP_'+k] = v + env['HTTP_' + key] = value return env def log_message(self, format, *args): diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index aadd290c33..78ce00511c 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -306,10 +306,8 @@ class RegexURLResolver(LocaleRegexProvider): tried.append([pattern]) else: if sub_match: - sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()]) - sub_match_dict.update(self.default_kwargs) - for k, v in sub_match.kwargs.iteritems(): - sub_match_dict[smart_str(k)] = v + sub_match_dict = dict(match.groupdict(), **self.default_kwargs) + sub_match_dict.update(sub_match.kwargs) return ResolverMatch(sub_match.func, sub_match.args, sub_match_dict, sub_match.url_name, self.app_name or sub_match.app_name, [self.namespace] + sub_match.namespaces) tried.append([pattern]) raise Resolver404({'tried': tried, 'path': new_path}) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 27623505e5..f26653f7b4 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -399,6 +399,9 @@ class BaseDatabaseFeatures(object): # in the SQL standard. supports_tablespaces = False + # Does the backend reset sequences between tests? + supports_sequence_reset = True + # Features that need to be confirmed at runtime # Cache whether the confirmation has been performed. _confirmed = False @@ -414,10 +417,11 @@ class BaseDatabaseFeatures(object): def confirm(self): "Perform manual checks of any database features that might vary between installs" - self._confirmed = True - self.supports_transactions = self._supports_transactions() - self.supports_stddev = self._supports_stddev() - self.can_introspect_foreign_keys = self._can_introspect_foreign_keys() + if not self._confirmed: + self._confirmed = True + self.supports_transactions = self._supports_transactions() + self.supports_stddev = self._supports_stddev() + self.can_introspect_foreign_keys = self._can_introspect_foreign_keys() def _supports_transactions(self): "Confirm support for transactions" @@ -439,8 +443,9 @@ class BaseDatabaseFeatures(object): try: self.connection.ops.check_aggregate_support(StdDevPop()) + return True except NotImplementedError: - self.supports_stddev = False + return False def _can_introspect_foreign_keys(self): "Confirm support for introspected foreign keys" diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index ecbb02f31d..1df487bd92 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -418,11 +418,20 @@ class DatabaseWrapper(BaseDatabaseWrapper): @cached_property def mysql_version(self): if not self.server_version: + new_connection = False if not self._valid_connection(): + # Ensure we have a connection with the DB by using a temporary + # cursor + new_connection = True self.cursor().close() - m = server_version_re.match(self.connection.get_server_info()) + server_info = self.connection.get_server_info() + if new_connection: + # Make sure we close the connection + self.connection.close() + self.connection = None + m = server_version_re.match(server_info) if not m: - raise Exception('Unable to determine MySQL version from version string %r' % self.connection.get_server_info()) + raise Exception('Unable to determine MySQL version from version string %r' % server_info) self.server_version = tuple([int(x) for x in m.groups()]) return self.server_version diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 2f3a43d698..fe512dfee5 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -83,6 +83,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): ignores_nulls_in_unique_constraints = False has_bulk_insert = True supports_tablespaces = True + supports_sequence_reset = False class DatabaseOperations(BaseDatabaseOperations): compiler_module = "django.db.backends.oracle.compiler" diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 7ce9dd38eb..4fba304e23 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -51,15 +51,15 @@ def adapt_datetime_with_timezone_support(value): default_timezone = timezone.get_default_timezone() value = timezone.make_aware(value, default_timezone) value = value.astimezone(timezone.utc).replace(tzinfo=None) - return value.isoformat(" ") + return value.isoformat(b" ") -Database.register_converter("bool", lambda s: str(s) == '1') -Database.register_converter("time", parse_time) -Database.register_converter("date", parse_date) -Database.register_converter("datetime", parse_datetime_with_timezone_support) -Database.register_converter("timestamp", parse_datetime_with_timezone_support) -Database.register_converter("TIMESTAMP", parse_datetime_with_timezone_support) -Database.register_converter("decimal", util.typecast_decimal) +Database.register_converter(b"bool", lambda s: str(s) == '1') +Database.register_converter(b"time", parse_time) +Database.register_converter(b"date", parse_date) +Database.register_converter(b"datetime", parse_datetime_with_timezone_support) +Database.register_converter(b"timestamp", parse_datetime_with_timezone_support) +Database.register_converter(b"TIMESTAMP", parse_datetime_with_timezone_support) +Database.register_converter(b"decimal", util.typecast_decimal) Database.register_adapter(datetime.datetime, adapt_datetime_with_timezone_support) Database.register_adapter(decimal.Decimal, util.rev_typecast_decimal) if Database.version_info >= (2, 4, 1): diff --git a/django/db/models/base.py b/django/db/models/base.py index a555ad9f13..13238fc9dc 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -57,11 +57,11 @@ class ModelBase(type): new_class.add_to_class('_meta', Options(meta, **kwargs)) if not abstract: - new_class.add_to_class('DoesNotExist', subclass_exception('DoesNotExist', + new_class.add_to_class('DoesNotExist', subclass_exception(b'DoesNotExist', tuple(x.DoesNotExist for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (ObjectDoesNotExist,), module)) - new_class.add_to_class('MultipleObjectsReturned', subclass_exception('MultipleObjectsReturned', + new_class.add_to_class('MultipleObjectsReturned', subclass_exception(b'MultipleObjectsReturned', tuple(x.MultipleObjectsReturned for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (MultipleObjectsReturned,), module)) @@ -404,7 +404,6 @@ class Model(object): # and as a result, the super call will cause an infinite recursion. # See #10547 and #12121. defers = [] - pk_val = None if self._deferred: from django.db.models.query_utils import deferred_class_factory factory = deferred_class_factory @@ -412,12 +411,7 @@ class Model(object): if isinstance(self.__class__.__dict__.get(field.attname), DeferredAttribute): defers.append(field.attname) - if pk_val is None: - # The pk_val and model values are the same for all - # DeferredAttribute classes, so we only need to do this - # once. - obj = self.__class__.__dict__[field.attname] - model = obj.model_ref() + model = self._meta.proxy_for_model else: factory = simple_class_factory return (model_unpickle, (model, defers, factory), data) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index f694958692..d572cce28f 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -1,3 +1,4 @@ +import collections import copy import datetime import decimal @@ -16,7 +17,7 @@ from django.utils.functional import curry, total_ordering from django.utils.text import capfirst from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from django.utils.encoding import smart_unicode, force_unicode, smart_str +from django.utils.encoding import smart_unicode, force_unicode from django.utils.ipv6 import clean_ipv6_address class NOT_PROVIDED: @@ -436,7 +437,7 @@ class Field(object): return bound_field_class(self, fieldmapping, original) def _get_choices(self): - if hasattr(self._choices, 'next'): + if isinstance(self._choices, collections.Iterator): choices, self._choices = tee(self._choices) return choices else: @@ -529,7 +530,7 @@ class AutoField(Field): try: return int(value) except (TypeError, ValueError): - msg = self.error_messages['invalid'] % str(value) + msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(msg) def validate(self, value, model_instance): @@ -581,7 +582,7 @@ class BooleanField(Field): return True if value in ('f', 'False', '0'): return False - msg = self.error_messages['invalid'] % str(value) + msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(msg) def get_prep_lookup(self, lookup_type, value): @@ -677,7 +678,7 @@ class DateField(Field): return value if isinstance(value, datetime.datetime): if settings.USE_TZ and timezone.is_aware(value): - # Convert aware datetimes to the current time zone + # Convert aware datetimes to the default time zone # before casting them to dates (#17742). default_timezone = timezone.get_default_timezone() value = timezone.make_naive(value, default_timezone) @@ -685,8 +686,6 @@ class DateField(Field): if isinstance(value, datetime.date): return value - value = smart_str(value) - try: parsed = parse_date(value) if parsed is not None: @@ -778,8 +777,6 @@ class DateTimeField(DateField): value = timezone.make_aware(value, default_timezone) return value - value = smart_str(value) - try: parsed = parse_datetime(value) if parsed is not None: @@ -861,7 +858,7 @@ class DecimalField(Field): try: return decimal.Decimal(value) except decimal.InvalidOperation: - msg = self.error_messages['invalid'] % str(value) + msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(msg) def _format(self, value): @@ -966,7 +963,7 @@ class FloatField(Field): try: return float(value) except (TypeError, ValueError): - msg = self.error_messages['invalid'] % str(value) + msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(msg) def formfield(self, **kwargs): @@ -1001,7 +998,7 @@ class IntegerField(Field): try: return int(value) except (TypeError, ValueError): - msg = self.error_messages['invalid'] % str(value) + msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(msg) def formfield(self, **kwargs): @@ -1106,7 +1103,7 @@ class NullBooleanField(Field): return True if value in ('f', 'False', '0'): return False - msg = self.error_messages['invalid'] % str(value) + msg = self.error_messages['invalid'] % value raise exceptions.ValidationError(msg) def get_prep_lookup(self, lookup_type, value): @@ -1227,8 +1224,6 @@ class TimeField(Field): # database backend (e.g. Oracle), so we'll be accommodating. return value.time() - value = smart_str(value) - try: parsed = parse_time(value) if parsed is not None: diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 6f1f183d4f..34cf84dbc0 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -3,6 +3,7 @@ import os from django import forms from django.db.models.fields import Field +from django.core.exceptions import ValidationError from django.core.files.base import File from django.core.files.storage import default_storage from django.core.files.images import ImageFile @@ -204,6 +205,11 @@ class FileDescriptor(object): instance.__dict__[self.field.name] = value class FileField(Field): + + default_error_messages = { + 'max_length': _(u'Filename is %(extra)d characters too long.') + } + # The class to wrap instance attributes in. Accessing the file object off # the instance will always return an instance of attr_class. attr_class = FieldFile @@ -226,6 +232,21 @@ class FileField(Field): kwargs['max_length'] = kwargs.get('max_length', 100) super(FileField, self).__init__(verbose_name, name, **kwargs) + def validate(self, value, model_instance): + """ + Validates that the generated file name still fits within max_length. + """ + # The generated file name stored in the database is generally longer + # than the uploaded file name. Using the length of generated name in + # the error message would be confusing. However, in the common case + # (ie. upload_to='path/to/upload/dir'), the length of the generated + # name equals the length of the uploaded name plus a constant. Thus + # we can tell the user how much shorter the name should be (roughly). + length = len(self.generate_filename(model_instance, value.name)) + if self.max_length and length > self.max_length: + error_values = {'extra': length - self.max_length} + raise ValidationError(self.error_messages['max_length'] % error_values) + def get_internal_type(self): return "FileField" diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index a16f9553c6..c4f95a12d2 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -237,13 +237,18 @@ class SingleRelatedObjectDescriptor(object): return self.related.model._base_manager.using(db) def get_prefetch_query_set(self, instances): - vals = set(instance._get_pk_val() for instance in instances) - params = {'%s__pk__in' % self.related.field.name: vals} - return (self.get_query_set(instance=instances[0]).filter(**params), - attrgetter(self.related.field.attname), - lambda obj: obj._get_pk_val(), - True, - self.cache_name) + rel_obj_attr = attrgetter(self.related.field.attname) + instance_attr = lambda obj: obj._get_pk_val() + instances_dict = dict((instance_attr(inst), inst) for inst in instances) + params = {'%s__pk__in' % self.related.field.name: instances_dict.keys()} + qs = self.get_query_set(instance=instances[0]).filter(**params) + # Since we're going to assign directly in the cache, + # we must manage the reverse relation cache manually. + rel_obj_cache_name = self.related.field.get_cache_name() + for rel_obj in qs: + instance = instances_dict[rel_obj_attr(rel_obj)] + setattr(rel_obj, rel_obj_cache_name, instance) + return qs, rel_obj_attr, instance_attr, True, self.cache_name def __get__(self, instance, instance_type=None): if instance is None: @@ -324,17 +329,23 @@ class ReverseSingleRelatedObjectDescriptor(object): return QuerySet(self.field.rel.to).using(db) def get_prefetch_query_set(self, instances): - vals = set(getattr(instance, self.field.attname) for instance in instances) + rel_obj_attr = attrgetter(self.field.rel.field_name) + instance_attr = attrgetter(self.field.attname) + instances_dict = dict((instance_attr(inst), inst) for inst in instances) other_field = self.field.rel.get_related_field() if other_field.rel: - params = {'%s__pk__in' % self.field.rel.field_name: vals} + params = {'%s__pk__in' % self.field.rel.field_name: instances_dict.keys()} else: - params = {'%s__in' % self.field.rel.field_name: vals} - return (self.get_query_set(instance=instances[0]).filter(**params), - attrgetter(self.field.rel.field_name), - attrgetter(self.field.attname), - True, - self.cache_name) + params = {'%s__in' % self.field.rel.field_name: instances_dict.keys()} + qs = self.get_query_set(instance=instances[0]).filter(**params) + # Since we're going to assign directly in the cache, + # we must manage the reverse relation cache manually. + if not self.field.rel.multiple: + rel_obj_cache_name = self.field.related.get_cache_name() + for rel_obj in qs: + instance = instances_dict[rel_obj_attr(rel_obj)] + setattr(rel_obj, rel_obj_cache_name, instance) + return qs, rel_obj_attr, instance_attr, True, self.cache_name def __get__(self, instance, instance_type=None): if instance is None: @@ -467,18 +478,24 @@ class ForeignRelatedObjectsDescriptor(object): return self.instance._prefetched_objects_cache[rel_field.related_query_name()] except (AttributeError, KeyError): db = self._db or router.db_for_read(self.model, instance=self.instance) - return super(RelatedManager, self).get_query_set().using(db).filter(**self.core_filters) + qs = super(RelatedManager, self).get_query_set().using(db).filter(**self.core_filters) + qs._known_related_object = (rel_field.name, self.instance) + return qs def get_prefetch_query_set(self, instances): + rel_obj_attr = attrgetter(rel_field.get_attname()) + instance_attr = attrgetter(attname) + instances_dict = dict((instance_attr(inst), inst) for inst in instances) db = self._db or router.db_for_read(self.model, instance=instances[0]) - query = {'%s__%s__in' % (rel_field.name, attname): - set(getattr(obj, attname) for obj in instances)} + query = {'%s__%s__in' % (rel_field.name, attname): instances_dict.keys()} qs = super(RelatedManager, self).get_query_set().using(db).filter(**query) - return (qs, - attrgetter(rel_field.get_attname()), - attrgetter(attname), - False, - rel_field.related_query_name()) + # Since we just bypassed this class' get_query_set(), we must manage + # the reverse relation manually. + for rel_obj in qs: + instance = instances_dict[rel_obj_attr(rel_obj)] + setattr(rel_obj, rel_field.name, instance) + cache_name = rel_field.related_query_name() + return qs, rel_obj_attr, instance_attr, False, cache_name def add(self, *objs): for obj in objs: diff --git a/django/db/models/query.py b/django/db/models/query.py index 65a36975d5..755820c3b0 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -41,6 +41,7 @@ class QuerySet(object): self._for_write = False self._prefetch_related_lookups = [] self._prefetch_done = False + self._known_related_object = None # (attname, rel_obj) ######################## # PYTHON MAGIC METHODS # @@ -282,9 +283,10 @@ class QuerySet(object): init_list.append(field.attname) model_cls = deferred_class_factory(self.model, skip) - # Cache db and model outside the loop + # Cache db, model and known_related_object outside the loop db = self.db model = self.model + kro_attname, kro_instance = self._known_related_object or (None, None) compiler = self.query.get_compiler(using=db) if fill_cache: klass_info = get_klass_info(model, max_depth=max_depth, @@ -294,12 +296,12 @@ class QuerySet(object): obj, _ = get_cached_row(row, index_start, db, klass_info, offset=len(aggregate_select)) else: + # Omit aggregates in object creation. + row_data = row[index_start:aggregate_start] if skip: - row_data = row[index_start:aggregate_start] obj = model_cls(**dict(zip(init_list, row_data))) else: - # Omit aggregates in object creation. - obj = model(*row[index_start:aggregate_start]) + obj = model(*row_data) # Store the source database of the object obj._state.db = db @@ -313,7 +315,11 @@ class QuerySet(object): # Add the aggregates to the model if aggregate_select: for i, aggregate in enumerate(aggregate_select): - setattr(obj, aggregate, row[i+aggregate_start]) + setattr(obj, aggregate, row[i + aggregate_start]) + + # Add the known related object to the model, if there is one + if kro_instance: + setattr(obj, kro_attname, kro_instance) yield obj @@ -864,6 +870,7 @@ class QuerySet(object): c = klass(model=self.model, query=query, using=self._db) c._for_write = self._for_write c._prefetch_related_lookups = self._prefetch_related_lookups[:] + c._known_related_object = self._known_related_object c.__dict__.update(kwargs) if setup and hasattr(c, '_setup_query'): c._setup_query() @@ -1781,9 +1788,7 @@ def prefetch_one_level(instances, prefetcher, attname): rel_obj_cache = {} for rel_obj in all_related_objects: rel_attr_val = rel_obj_attr(rel_obj) - if rel_attr_val not in rel_obj_cache: - rel_obj_cache[rel_attr_val] = [] - rel_obj_cache[rel_attr_val].append(rel_obj) + rel_obj_cache.setdefault(rel_attr_val, []).append(rel_obj) for obj in instances: instance_attr_val = instance_attr(obj) diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index a56ab5ca80..5676fdce9a 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -6,8 +6,6 @@ large and/or so that they can be used by other modules without getting into circular import difficulties. """ -import weakref - from django.db.backends import util from django.utils import tree @@ -70,8 +68,6 @@ class DeferredAttribute(object): """ def __init__(self, field_name, model): self.field_name = field_name - self.model_ref = weakref.ref(model) - self.loaded = False def __get__(self, instance, owner): """ @@ -79,27 +75,32 @@ class DeferredAttribute(object): Returns the cached value. """ from django.db.models.fields import FieldDoesNotExist + non_deferred_model = instance._meta.proxy_for_model + opts = non_deferred_model._meta assert instance is not None - cls = self.model_ref() data = instance.__dict__ if data.get(self.field_name, self) is self: # self.field_name is the attname of the field, but only() takes the # actual name, so we need to translate it here. try: - cls._meta.get_field_by_name(self.field_name) - name = self.field_name + f = opts.get_field_by_name(self.field_name)[0] except FieldDoesNotExist: - name = [f.name for f in cls._meta.fields - if f.attname == self.field_name][0] - # We use only() instead of values() here because we want the - # various data coersion methods (to_python(), etc.) to be called - # here. - val = getattr( - cls._base_manager.filter(pk=instance.pk).only(name).using( - instance._state.db).get(), - self.field_name - ) + f = [f for f in opts.fields + if f.attname == self.field_name][0] + name = f.name + # Lets see if the field is part of the parent chain. If so we + # might be able to reuse the already loaded value. Refs #18343. + val = self._check_parent_chain(instance, name) + if val is None: + # We use only() instead of values() here because we want the + # various data coersion methods (to_python(), etc.) to be + # called here. + val = getattr( + non_deferred_model._base_manager.only(name).using( + instance._state.db).get(pk=instance.pk), + self.field_name + ) data[self.field_name] = val return data[self.field_name] @@ -110,6 +111,20 @@ class DeferredAttribute(object): """ instance.__dict__[self.field_name] = value + def _check_parent_chain(self, instance, name): + """ + Check if the field value can be fetched from a parent field already + loaded in the instance. This can be done if the to-be fetched + field is a primary key field. + """ + opts = instance._meta + f = opts.get_field_by_name(name)[0] + link_field = opts.get_ancestor_link(f.model) + if f.primary_key and f != link_field: + return getattr(instance, link_field.attname) + return None + + def select_related_descend(field, restricted, requested, reverse=False): """ Returns True if this field should be used to descend deeper for diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index 3cd26d3bc9..5801b2f428 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -455,6 +455,9 @@ class SQLCompiler(object): alias = self.query.get_initial_alias() field, target, opts, joins, _, _ = self.query.setup_joins(pieces, opts, alias, False) + # We will later on need to promote those joins that were added to the + # query afresh above. + joins_to_promote = [j for j in joins if self.query.alias_refcount[j] < 2] alias = joins[-1] col = target.column if not field.rel: @@ -466,8 +469,9 @@ class SQLCompiler(object): # Must use left outer joins for nullable fields and their relations. # Ordering or distinct must not affect the returned set, and INNER # JOINS for nullable fields could do this. - self.query.promote_alias_chain(joins, - self.query.alias_map[joins[0]].join_type == self.query.LOUTER) + if joins_to_promote: + self.query.promote_alias_chain(joins_to_promote, + self.query.alias_map[joins_to_promote[0]].join_type == self.query.LOUTER) return field, col, alias, joins, opts def _final_join_removal(self, col, alias): @@ -1015,6 +1019,12 @@ class SQLUpdateCompiler(SQLCompiler): query.extra = {} query.select = [] query.add_fields([query.model._meta.pk.name]) + # Recheck the count - it is possible that fiddling with the select + # fields above removes tables from the query. Refs #18304. + count = query.count_active_tables() + if not self.query.related_updates and count == 1: + return + must_pre_select = count > 1 and not self.connection.features.update_can_self_select # Now we adjust the current query: reset the where clause and get rid diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 0bc3638b80..5515bc4f83 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -4,6 +4,7 @@ Code to manage the creation and SQL rendering of 'where' constraints. from __future__ import absolute_import +import collections import datetime from itertools import repeat @@ -49,7 +50,7 @@ class WhereNode(tree.Node): return obj, lookup_type, value = data - if hasattr(value, '__iter__') and hasattr(value, 'next'): + if isinstance(value, collections.Iterator): # Consume any generators immediately, so that we can determine # emptiness and transform any non-empty values correctly. value = list(value) diff --git a/django/dispatch/saferef.py b/django/dispatch/saferef.py index e060e619f8..2a2c8739fb 100644 --- a/django/dispatch/saferef.py +++ b/django/dispatch/saferef.py @@ -155,12 +155,12 @@ class BoundMethodWeakref(object): def __nonzero__( self ): """Whether we are still a valid reference""" return self() is not None - - def __cmp__( self, other ): + + def __eq__(self, other): """Compare with another reference""" - if not isinstance (other,self.__class__): - return cmp( self.__class__, type(other) ) - return cmp( self.key, other.key) + if not isinstance(other, self.__class__): + return self.__class__ == type(other) + return self.key == other.key def __call__(self): """Return a strong reference to the bound method diff --git a/django/forms/fields.py b/django/forms/fields.py index 61137f55c1..3811510326 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -446,7 +446,7 @@ class RegexField(CharField): def _set_regex(self, regex): if isinstance(regex, basestring): - regex = re.compile(regex) + regex = re.compile(regex, re.UNICODE) self._regex = regex if hasattr(self, '_regex_validator') and self._regex_validator in self.validators: self.validators.remove(self._regex_validator) @@ -932,12 +932,16 @@ class FilePathField(ChoiceField): self.choices.append((f, f.replace(path, "", 1))) if self.allow_folders: for f in dirs: + if f == '__pycache__': + continue if self.match is None or self.match_re.search(f): f = os.path.join(root, f) self.choices.append((f, f.replace(path, "", 1))) else: try: for f in sorted(os.listdir(self.path)): + if f == '__pycache__': + continue full_file = os.path.join(self.path, f) if (((self.allow_files and os.path.isfile(full_file)) or (self.allow_folders and os.path.isdir(full_file))) and diff --git a/django/forms/models.py b/django/forms/models.py index ea80f8d855..5fcf9590eb 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -388,10 +388,10 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, parent = (object,) if hasattr(form, 'Meta'): parent = (form.Meta, object) - Meta = type('Meta', parent, attrs) + Meta = type(b'Meta', parent, attrs) # Give this new form class a reasonable name. - class_name = model.__name__ + 'Form' + class_name = model.__name__ + b'Form' # Class attributes for the new form class. form_class_attrs = { diff --git a/django/http/__init__.py b/django/http/__init__.py index 2bad146010..bf848deb38 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -18,7 +18,7 @@ _cookie_encodes_correctly = Cookie.SimpleCookie().value_encode(';') == (';', '"\ # See ticket #13007, http://bugs.python.org/issue2193 and http://trac.edgewall.org/ticket/2256 _tc = Cookie.SimpleCookie() try: - _tc.load('foo:bar=1') + _tc.load(b'foo:bar=1') _cookie_allows_colon_in_names = True except Cookie.CookieError: _cookie_allows_colon_in_names = False @@ -650,8 +650,8 @@ class HttpResponse(object): def _get_content(self): if self.has_header('Content-Encoding'): - return ''.join([str(e) for e in self._container]) - return ''.join([smart_str(e, self._charset) for e in self._container]) + return b''.join([str(e) for e in self._container]) + return b''.join([smart_str(e, self._charset) for e in self._container]) def _set_content(self, value): if hasattr(value, '__iter__'): diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index d09c8fba86..162c08e38d 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -268,7 +268,7 @@ class LazyStream(object): """ self._producer = producer self._empty = False - self._leftover = '' + self._leftover = b'' self.length = length self.position = 0 self._remaining = length @@ -282,7 +282,7 @@ class LazyStream(object): remaining = (size is not None and [size] or [self._remaining])[0] # do the whole thing in one shot if no limit was provided. if remaining is None: - yield ''.join(self) + yield b''.join(self) return # otherwise do some bookkeeping to return exactly enough @@ -298,7 +298,7 @@ class LazyStream(object): remaining -= len(emitting) yield emitting - out = ''.join(parts()) + out = b''.join(parts()) return out def next(self): @@ -311,7 +311,7 @@ class LazyStream(object): """ if self._leftover: output = self._leftover - self._leftover = '' + self._leftover = b'' else: output = next(self._producer) self._unget_history = [] @@ -341,7 +341,7 @@ class LazyStream(object): return self._update_unget_history(len(bytes)) self.position -= len(bytes) - self._leftover = ''.join([bytes, self._leftover]) + self._leftover = b''.join([bytes, self._leftover]) def _update_unget_history(self, num_bytes): """ @@ -459,7 +459,7 @@ class BoundaryIter(object): if not chunks: raise StopIteration() - chunk = ''.join(chunks) + chunk = b''.join(chunks) boundary = self._find_boundary(chunk, len(chunk) < self._rollback) if boundary: @@ -495,9 +495,9 @@ class BoundaryIter(object): end = index next = index + len(self._boundary) # backup over CRLF - if data[max(0,end-1)] == '\n': + if data[max(0,end-1)] == b'\n': end -= 1 - if data[max(0,end-1)] == '\r': + if data[max(0,end-1)] == b'\r': end -= 1 return end, next @@ -531,7 +531,7 @@ def parse_boundary_stream(stream, max_header_size): # 'find' returns the top of these four bytes, so we'll # need to munch them later to prevent them from polluting # the payload. - header_end = chunk.find('\r\n\r\n') + header_end = chunk.find(b'\r\n\r\n') def _parse_header(line): main_value_pair, params = parse_header(line) @@ -557,7 +557,7 @@ def parse_boundary_stream(stream, max_header_size): outdict = {} # Eliminate blank lines - for line in header.split('\r\n'): + for line in header.split(b'\r\n'): # This terminology ("main value" and "dictionary of # parameters") is from the Python docs. try: @@ -580,7 +580,7 @@ def parse_boundary_stream(stream, max_header_size): class Parser(object): def __init__(self, stream, boundary): self._stream = stream - self._separator = '--' + boundary + self._separator = b'--' + boundary def __iter__(self): boundarystream = InterBoundaryIter(self._stream, self._separator) @@ -590,27 +590,27 @@ class Parser(object): def parse_header(line): """ Parse the header into a key-value. """ - plist = _parse_header_params(';' + line) + plist = _parse_header_params(b';' + line) key = plist.pop(0).lower() pdict = {} for p in plist: - i = p.find('=') + i = p.find(b'=') if i >= 0: name = p[:i].strip().lower() value = p[i+1:].strip() - if len(value) >= 2 and value[0] == value[-1] == '"': + if len(value) >= 2 and value[0] == value[-1] == b'"': value = value[1:-1] - value = value.replace('\\\\', '\\').replace('\\"', '"') + value = value.replace(b'\\\\', b'\\').replace(b'\\"', b'"') pdict[name] = value return key, pdict def _parse_header_params(s): plist = [] - while s[:1] == ';': + while s[:1] == b';': s = s[1:] - end = s.find(';') - while end > 0 and s.count('"', 0, end) % 2: - end = s.find(';', end + 1) + end = s.find(b';') + while end > 0 and s.count(b'"', 0, end) % 2: + end = s.find(b';', end + 1) if end < 0: end = len(s) f = s[:end] diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py index d6b3a17c8c..6e0f079de7 100644 --- a/django/template/loaders/app_directories.py +++ b/django/template/loaders/app_directories.py @@ -52,7 +52,7 @@ class Loader(BaseLoader): def load_template_source(self, template_name, template_dirs=None): for filepath in self.get_template_sources(template_name, template_dirs): try: - with open(filepath) as fp: + with open(filepath, 'rb') as fp: return (fp.read().decode(settings.FILE_CHARSET), filepath) except IOError: pass diff --git a/django/template/loaders/filesystem.py b/django/template/loaders/filesystem.py index 8352406424..e87cfef987 100644 --- a/django/template/loaders/filesystem.py +++ b/django/template/loaders/filesystem.py @@ -34,7 +34,7 @@ class Loader(BaseLoader): tried = [] for filepath in self.get_template_sources(template_name, template_dirs): try: - with open(filepath) as fp: + with open(filepath, 'rb') as fp: return (fp.read().decode(settings.FILE_CHARSET), filepath) except IOError: tried.append(filepath) diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index ca79f41edc..e090781080 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -148,7 +148,7 @@ class BlockTranslateNode(Node): context.pop() try: result = result % data - except KeyError: + except (KeyError, ValueError): with translation.override(None): result = self.render(context) return result diff --git a/django/test/client.py b/django/test/client.py index c1764e2394..3cc6a6e537 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -155,7 +155,6 @@ def encode_file(boundary, key, file): ] - class RequestFactory(object): """ Class that lets you create mock Request objects for use in testing. @@ -227,7 +226,7 @@ class RequestFactory(object): return urllib.unquote(parsed[2]) def get(self, path, data={}, **extra): - "Construct a GET request" + "Construct a GET request." parsed = urlparse(path) r = { @@ -270,49 +269,39 @@ class RequestFactory(object): r.update(extra) return self.request(**r) - def options(self, path, data={}, **extra): - "Constrict an OPTIONS request" + def options(self, path, data='', content_type='application/octet-stream', + **extra): + "Construct an OPTIONS request." + return self.generic('OPTIONS', path, data, content_type, **extra) - parsed = urlparse(path) - r = { - 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], - 'REQUEST_METHOD': 'OPTIONS', - } - r.update(extra) - return self.request(**r) - - def put(self, path, data={}, content_type=MULTIPART_CONTENT, + def put(self, path, data='', content_type='application/octet-stream', **extra): "Construct a PUT request." + return self.generic('PUT', path, data, content_type, **extra) - put_data = self._encode_data(data, content_type) + def delete(self, path, data='', content_type='application/octet-stream', + **extra): + "Construct a DELETE request." + return self.generic('DELETE', path, data, content_type, **extra) + def generic(self, method, path, + data='', content_type='application/octet-stream', **extra): parsed = urlparse(path) + data = smart_str(data, settings.DEFAULT_CHARSET) r = { - 'CONTENT_LENGTH': len(put_data), - 'CONTENT_TYPE': content_type, 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': parsed[4], - 'REQUEST_METHOD': 'PUT', - 'wsgi.input': FakePayload(put_data), + 'REQUEST_METHOD': method, } + if data: + r.update({ + 'CONTENT_LENGTH': len(data), + 'CONTENT_TYPE': content_type, + 'wsgi.input': FakePayload(data), + }) r.update(extra) return self.request(**r) - def delete(self, path, data={}, **extra): - "Construct a DELETE request." - - parsed = urlparse(path) - r = { - 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], - 'REQUEST_METHOD': 'DELETE', - } - r.update(extra) - return self.request(**r) - - class Client(RequestFactory): """ A class that can act as a client for testing purposes. @@ -445,30 +434,35 @@ class Client(RequestFactory): response = self._handle_redirects(response, **extra) return response - def options(self, path, data={}, follow=False, **extra): + def options(self, path, data='', content_type='application/octet-stream', + follow=False, **extra): """ Request a response from the server using OPTIONS. """ - response = super(Client, self).options(path, data=data, **extra) + response = super(Client, self).options(path, + data=data, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) return response - def put(self, path, data={}, content_type=MULTIPART_CONTENT, + def put(self, path, data='', content_type='application/octet-stream', follow=False, **extra): """ Send a resource to the server using PUT. """ - response = super(Client, self).put(path, data=data, content_type=content_type, **extra) + response = super(Client, self).put(path, + data=data, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) return response - def delete(self, path, data={}, follow=False, **extra): + def delete(self, path, data='', content_type='application/octet-stream', + follow=False, **extra): """ Send a DELETE request to the server. """ - response = super(Client, self).delete(path, data=data, **extra) + response = super(Client, self).delete(path, + data=data, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) return response diff --git a/django/utils/cache.py b/django/utils/cache.py index 7ef3680db2..8b81a2ffa2 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -23,7 +23,7 @@ import time from django.conf import settings from django.core.cache import get_cache -from django.utils.encoding import smart_str, iri_to_uri, force_unicode +from django.utils.encoding import iri_to_uri, force_unicode from django.utils.http import http_date from django.utils.timezone import get_current_timezone_name from django.utils.translation import get_language @@ -53,7 +53,7 @@ def patch_cache_control(response, **kwargs): if t[1] is True: return t[0] else: - return t[0] + '=' + smart_str(t[1]) + return '%s=%s' % (t[0], t[1]) if response.has_header('Cache-Control'): cc = cc_delim_re.split(response['Cache-Control']) diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 44b7faf492..9d6486c601 100644 --- a/django/utils/crypto.py +++ b/django/utils/crypto.py @@ -8,6 +8,7 @@ import hashlib import binascii import operator import time +from functools import reduce # Use the system PRNG if possible import random @@ -23,8 +24,8 @@ except NotImplementedError: from django.conf import settings -_trans_5c = "".join([chr(x ^ 0x5C) for x in xrange(256)]) -_trans_36 = "".join([chr(x ^ 0x36) for x in xrange(256)]) +_trans_5c = b"".join([chr(x ^ 0x5C) for x in xrange(256)]) +_trans_36 = b"".join([chr(x ^ 0x36) for x in xrange(256)]) def salted_hmac(key_salt, value, secret=None): @@ -148,11 +149,11 @@ def pbkdf2(password, salt, iterations, dklen=0, digest=None): def F(i): def U(): - u = salt + struct.pack('>I', i) + u = salt + struct.pack(b'>I', i) for j in xrange(int(iterations)): u = _fast_hmac(password, u, digest).digest() yield _bin_to_long(u) return _long_to_bin(reduce(operator.xor, U()), hex_format_string) T = [F(x) for x in range(1, l + 1)] - return ''.join(T[:-1]) + T[-1][:r] + return b''.join(T[:-1]) + T[-1][:r] diff --git a/django/utils/encoding.py b/django/utils/encoding.py index d9b59448a9..6b246ac59b 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -1,4 +1,3 @@ -import types import urllib import locale import datetime @@ -45,7 +44,7 @@ def is_protected_type(obj): force_unicode(strings_only=True). """ return isinstance(obj, ( - types.NoneType, + type(None), int, long, datetime.datetime, datetime.date, datetime.time, float, Decimal) @@ -107,7 +106,7 @@ def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): If strings_only is True, don't convert (some) non-string-like objects. """ - if strings_only and isinstance(s, (types.NoneType, int)): + if strings_only and (s is None or isinstance(s, int)): return s if isinstance(s, Promise): return unicode(s).encode(encoding, errors) @@ -154,7 +153,7 @@ def iri_to_uri(iri): # converted. if iri is None: return iri - return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~") + return urllib.quote(smart_str(iri), safe=b"/#%[]=:;$&()+,!?*@'~") def filepath_to_uri(path): """Convert an file system path to a URI portion that is suitable for @@ -173,7 +172,7 @@ def filepath_to_uri(path): return path # I know about `os.sep` and `os.altsep` but I want to leave # some flexibility for hardcoding separators. - return urllib.quote(smart_str(path).replace("\\", "/"), safe="/~!*()'") + return urllib.quote(smart_str(path).replace("\\", "/"), safe=b"/~!*()'") # The encoding of the default system locale but falls back to the # given fallback encoding if the encoding is unsupported by python or could diff --git a/django/utils/functional.py b/django/utils/functional.py index 5b88c3f3cb..31466ee8b8 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -58,6 +58,7 @@ def lazy(func, *resultclasses): function is evaluated on every access. """ + @total_ordering class __proxy__(Promise): """ Encapsulate a function call and act as a proxy for methods that are @@ -124,17 +125,23 @@ def lazy(func, *resultclasses): def __str_cast(self): return str(func(*self.__args, **self.__kw)) - def __cmp__(self, rhs): + def __cast(self): if self._delegate_str: - s = str(func(*self.__args, **self.__kw)) + return self.__str_cast() elif self._delegate_unicode: - s = unicode(func(*self.__args, **self.__kw)) + return self.__unicode_cast() else: - s = func(*self.__args, **self.__kw) - if isinstance(rhs, Promise): - return -cmp(rhs, s) - else: - return cmp(s, rhs) + return func(*self.__args, **self.__kw) + + def __eq__(self, other): + if isinstance(other, Promise): + other = other.__cast() + return self.__cast() == other + + def __lt__(self, other): + if isinstance(other, Promise): + other = other.__cast() + return self.__cast() < other def __mod__(self, rhs): if self._delegate_str: diff --git a/django/utils/html.py b/django/utils/html.py index 6760f7adad..1b1a16a911 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -116,7 +116,7 @@ def smart_urlquote(url): # contains a % not followed by two hexadecimal digits. See #9655. if '%' not in url or unquoted_percents_re.search(url): # See http://bugs.python.org/issue2637 - url = urllib.quote(smart_str(url), safe='!*\'();:@&=+$,/?#[]~') + url = urllib.quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~') return force_unicode(url) diff --git a/django/views/debug.py b/django/views/debug.py index 418b2bab9f..d95cd62017 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -155,9 +155,20 @@ class SafeExceptionReporterFilter(ExceptionReporterFilter): Replaces the values of variables marked as sensitive with stars (*********). """ - func_name = tb_frame.f_code.co_name - func = tb_frame.f_globals.get(func_name) - sensitive_variables = getattr(func, 'sensitive_variables', []) + # Loop through the frame's callers to see if the sensitive_variables + # decorator was used. + current_frame = tb_frame.f_back + sensitive_variables = None + while current_frame is not None: + if (current_frame.f_code.co_name == 'sensitive_variables_wrapper' + and 'sensitive_variables_wrapper' in current_frame.f_locals): + # The sensitive_variables decorator was used, so we take note + # of the sensitive variables' names. + wrapper = current_frame.f_locals['sensitive_variables_wrapper'] + sensitive_variables = getattr(wrapper, 'sensitive_variables', None) + break + current_frame = current_frame.f_back + cleansed = [] if self.is_active(request) and sensitive_variables: if sensitive_variables == '__ALL__': @@ -333,7 +344,7 @@ class ExceptionReporter(object): source = source.splitlines() if source is None: try: - with open(filename) as fp: + with open(filename, 'rb') as fp: source = fp.readlines() except (OSError, IOError): pass diff --git a/django/views/decorators/debug.py b/django/views/decorators/debug.py index d04967ef09..5c222963d3 100644 --- a/django/views/decorators/debug.py +++ b/django/views/decorators/debug.py @@ -26,13 +26,13 @@ def sensitive_variables(*variables): """ def decorator(func): @functools.wraps(func) - def wrapper(*args, **kwargs): + def sensitive_variables_wrapper(*args, **kwargs): if variables: - wrapper.sensitive_variables = variables + sensitive_variables_wrapper.sensitive_variables = variables else: - wrapper.sensitive_variables = '__ALL__' + sensitive_variables_wrapper.sensitive_variables = '__ALL__' return func(*args, **kwargs) - return wrapper + return sensitive_variables_wrapper return decorator @@ -61,11 +61,11 @@ def sensitive_post_parameters(*parameters): """ def decorator(view): @functools.wraps(view) - def wrapper(request, *args, **kwargs): + def sensitive_post_parameters_wrapper(request, *args, **kwargs): if parameters: request.sensitive_post_parameters = parameters else: request.sensitive_post_parameters = '__ALL__' return view(request, *args, **kwargs) - return wrapper + return sensitive_post_parameters_wrapper return decorator diff --git a/django/views/generic/base.py b/django/views/generic/base.py index c32a58a349..7d3649cbff 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -79,14 +79,22 @@ class View(object): return handler(request, *args, **kwargs) def http_method_not_allowed(self, request, *args, **kwargs): - allowed_methods = [m for m in self.http_method_names if hasattr(self, m)] logger.warning('Method Not Allowed (%s): %s', request.method, request.path, extra={ 'status_code': 405, 'request': self.request } ) - return http.HttpResponseNotAllowed(allowed_methods) + return http.HttpResponseNotAllowed(self._allowed_methods()) + + def options(self, request, *args, **kwargs): + response = http.HttpResponse() + response['Allow'] = ', '.join(self._allowed_methods()) + response['Content-Length'] = 0 + return response + + def _allowed_methods(self): + return [m.upper() for m in self.http_method_names if hasattr(self, m)] class TemplateResponseMixin(object): diff --git a/django/views/generic/dates.py b/django/views/generic/dates.py index 6964624516..3e6ab0fa0c 100644 --- a/django/views/generic/dates.py +++ b/django/views/generic/dates.py @@ -23,7 +23,9 @@ class YearMixin(object): return self.year_format def get_year(self): - "Return the year for which this view should display data" + """ + Return the year for which this view should display data. + """ year = self.year if year is None: try: @@ -35,6 +37,32 @@ class YearMixin(object): raise Http404(_(u"No year specified")) return year + def get_next_year(self, date): + """ + Get the next valid year. + """ + return _get_next_prev(self, date, is_previous=False, period='year') + + def get_previous_year(self, date): + """ + Get the previous valid year. + """ + return _get_next_prev(self, date, is_previous=True, period='year') + + def _get_next_year(self, date): + """ + Return the start date of the next interval. + + The interval is defined by start date <= item date < next start date. + """ + return date.replace(year=date.year + 1, month=1, day=1) + + def _get_current_year(self, date): + """ + Return the start date of the current interval. + """ + return date.replace(month=1, day=1) + class MonthMixin(object): month_format = '%b' @@ -48,7 +76,9 @@ class MonthMixin(object): return self.month_format def get_month(self): - "Return the month for which this view should display data" + """ + Return the month for which this view should display data. + """ month = self.month if month is None: try: @@ -64,20 +94,30 @@ class MonthMixin(object): """ Get the next valid month. """ - # next must be the first day of the next month. - if date.month == 12: - next = date.replace(year=date.year + 1, month=1, day=1) - else: - next = date.replace(month=date.month + 1, day=1) - return _get_next_prev(self, next, is_previous=False, period='month') + return _get_next_prev(self, date, is_previous=False, period='month') def get_previous_month(self, date): """ Get the previous valid month. """ - # prev must be the last day of the previous month. - prev = date.replace(day=1) - datetime.timedelta(days=1) - return _get_next_prev(self, prev, is_previous=True, period='month') + return _get_next_prev(self, date, is_previous=True, period='month') + + def _get_next_month(self, date): + """ + Return the start date of the next interval. + + The interval is defined by start date <= item date < next start date. + """ + if date.month == 12: + return date.replace(year=date.year + 1, month=1, day=1) + else: + return date.replace(month=date.month + 1, day=1) + + def _get_current_month(self, date): + """ + Return the start date of the previous interval. + """ + return date.replace(day=1) class DayMixin(object): @@ -92,7 +132,9 @@ class DayMixin(object): return self.day_format def get_day(self): - "Return the day for which this view should display data" + """ + Return the day for which this view should display data. + """ day = self.day if day is None: try: @@ -108,15 +150,27 @@ class DayMixin(object): """ Get the next valid day. """ - next = date + datetime.timedelta(days=1) - return _get_next_prev(self, next, is_previous=False, period='day') + return _get_next_prev(self, date, is_previous=False, period='day') def get_previous_day(self, date): """ Get the previous valid day. """ - prev = date - datetime.timedelta(days=1) - return _get_next_prev(self, prev, is_previous=True, period='day') + return _get_next_prev(self, date, is_previous=True, period='day') + + def _get_next_day(self, date): + """ + Return the start date of the next interval. + + The interval is defined by start date <= item date < next start date. + """ + return date + datetime.timedelta(days=1) + + def _get_current_day(self, date): + """ + Return the start date of the current interval. + """ + return date class WeekMixin(object): @@ -131,7 +185,9 @@ class WeekMixin(object): return self.week_format def get_week(self): - "Return the week for which this view should display data" + """ + Return the week for which this view should display data + """ week = self.week if week is None: try: @@ -147,19 +203,34 @@ class WeekMixin(object): """ Get the next valid week. """ - # next must be the first day of the next week. - next = date + datetime.timedelta(days=7 - self._get_weekday(date)) - return _get_next_prev(self, next, is_previous=False, period='week') + return _get_next_prev(self, date, is_previous=False, period='week') def get_previous_week(self, date): """ Get the previous valid week. """ - # prev must be the last day of the previous week. - prev = date - datetime.timedelta(days=self._get_weekday(date) + 1) - return _get_next_prev(self, prev, is_previous=True, period='week') + return _get_next_prev(self, date, is_previous=True, period='week') + + def _get_next_week(self, date): + """ + Return the start date of the next interval. + + The interval is defined by start date <= item date < next start date. + """ + return date + datetime.timedelta(days=7 - self._get_weekday(date)) + + def _get_current_week(self, date): + """ + Return the start date of the current interval. + """ + return date - datetime.timedelta(self._get_weekday(date)) def _get_weekday(self, date): + """ + Return the weekday for a given date. + + The first day according to the week format is 0 and the last day is 6. + """ week_format = self.get_week_format() if week_format == '%W': # week starts on Monday return date.weekday() @@ -168,6 +239,7 @@ class WeekMixin(object): else: raise ValueError("unknown week format: %s" % week_format) + class DateMixin(object): """ Mixin class for views manipulating date-based data. @@ -255,7 +327,7 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): """ raise NotImplementedError('A DateView must provide an implementation of get_dated_items()') - def get_dated_queryset(self, **lookup): + def get_dated_queryset(self, ordering=None, **lookup): """ Get a queryset properly filtered according to `allow_future` and any extra lookup kwargs. @@ -266,14 +338,17 @@ class BaseDateListView(MultipleObjectMixin, DateMixin, View): allow_empty = self.get_allow_empty() paginate_by = self.get_paginate_by(qs) + if ordering is not None: + qs = qs.order_by(ordering) + if not allow_future: - now = timezone.now() if self.uses_datetime_field else datetime.date.today() + now = timezone.now() if self.uses_datetime_field else timezone_today() qs = qs.filter(**{'%s__lte' % date_field: now}) if not allow_empty: # When pagination is enabled, it's better to do a cheap query # than to load the unpaginated queryset in memory. - is_empty = not bool(qs) if paginate_by is None else not qs.exists() + is_empty = len(qs) == 0 if paginate_by is None else not qs.exists() if is_empty: raise Http404(_(u"No %(verbose_name_plural)s available") % { 'verbose_name_plural': force_unicode(qs.model._meta.verbose_name_plural) @@ -310,15 +385,13 @@ class BaseArchiveIndexView(BaseDateListView): """ Return (date_list, items, extra_context) for this request. """ - qs = self.get_dated_queryset() + qs = self.get_dated_queryset(ordering='-%s' % self.get_date_field()) date_list = self.get_date_list(qs, 'year') - if date_list: - object_list = qs.order_by('-' + self.get_date_field()) - else: - object_list = qs.none() + if not date_list: + qs = qs.none() - return (date_list, object_list, {}) + return (date_list, qs, {}) class ArchiveIndexView(MultipleObjectTemplateResponseMixin, BaseArchiveIndexView): @@ -344,23 +417,25 @@ class BaseYearArchiveView(YearMixin, BaseDateListView): date = _date_from_string(year, self.get_year_format()) since = self._make_date_lookup_arg(date) - until = self._make_date_lookup_arg(datetime.date(date.year + 1, 1, 1)) + until = self._make_date_lookup_arg(self._get_next_year(date)) lookup_kwargs = { '%s__gte' % date_field: since, '%s__lt' % date_field: until, } - qs = self.get_dated_queryset(**lookup_kwargs) + qs = self.get_dated_queryset(ordering='-%s' % date_field, **lookup_kwargs) date_list = self.get_date_list(qs, 'month') - if self.get_make_object_list(): - object_list = qs.order_by('-' + date_field) - else: + if not self.get_make_object_list(): # We need this to be a queryset since parent classes introspect it # to find information about the model. - object_list = qs.none() + qs = qs.none() - return (date_list, object_list, {'year': year}) + return (date_list, qs, { + 'year': date, + 'next_year': self.get_next_year(date), + 'previous_year': self.get_previous_year(date), + }) def get_make_object_list(self): """ @@ -392,12 +467,8 @@ class BaseMonthArchiveView(YearMixin, MonthMixin, BaseDateListView): date = _date_from_string(year, self.get_year_format(), month, self.get_month_format()) - # Construct a date-range lookup. since = self._make_date_lookup_arg(date) - if date.month == 12: - until = self._make_date_lookup_arg(datetime.date(date.year + 1, 1, 1)) - else: - until = self._make_date_lookup_arg(datetime.date(date.year, date.month + 1, 1)) + until = self._make_date_lookup_arg(self._get_next_month(date)) lookup_kwargs = { '%s__gte' % date_field: since, '%s__lt' % date_field: until, @@ -442,9 +513,8 @@ class BaseWeekArchiveView(YearMixin, WeekMixin, BaseDateListView): week_start, '%w', week, week_format) - # Construct a date-range lookup. since = self._make_date_lookup_arg(date) - until = self._make_date_lookup_arg(date + datetime.timedelta(days=7)) + until = self._make_date_lookup_arg(self._get_next_week(date)) lookup_kwargs = { '%s__gte' % date_field: since, '%s__lt' % date_field: until, @@ -585,22 +655,22 @@ def _date_from_string(year, year_format, month='', month_format='', day='', day_ }) -def _get_next_prev(generic_view, naive_result, is_previous, period): +def _get_next_prev(generic_view, date, is_previous, period): """ Helper: Get the next or the previous valid date. The idea is to allow links on month/day views to never be 404s by never providing a date that'll be invalid for the given view. - This is a bit complicated since it handles both next and previous months - and days (for MonthArchiveView and DayArchiveView); hence the coupling to generic_view. + This is a bit complicated since it handles different intervals of time, + hence the coupling to generic_view. However in essence the logic comes down to: * If allow_empty and allow_future are both true, this is easy: just - return the naive result (just the next/previous day or month, + return the naive result (just the next/previous day/week/month, reguardless of object existence.) - * If allow_empty is true, allow_future is false, and the naive month + * If allow_empty is true, allow_future is false, and the naive result isn't in the future, then return it; otherwise return None. * If allow_empty is false and allow_future is true, return the next @@ -616,9 +686,23 @@ def _get_next_prev(generic_view, naive_result, is_previous, period): allow_empty = generic_view.get_allow_empty() allow_future = generic_view.get_allow_future() - # If allow_empty is True the naive value will be valid + get_current = getattr(generic_view, '_get_current_%s' % period) + get_next = getattr(generic_view, '_get_next_%s' % period) + + # Bounds of the current interval + start, end = get_current(date), get_next(date) + + # If allow_empty is True, the naive result will be valid if allow_empty: - result = naive_result + if is_previous: + result = get_current(start - datetime.timedelta(days=1)) + else: + result = end + + if allow_future or result <= timezone_today(): + return result + else: + return None # Otherwise, we'll need to go to the database to look for an object # whose date_field is at least (greater than/less than) the given @@ -627,12 +711,22 @@ def _get_next_prev(generic_view, naive_result, is_previous, period): # Construct a lookup and an ordering depending on whether we're doing # a previous date or a next date lookup. if is_previous: - lookup = {'%s__lte' % date_field: generic_view._make_date_lookup_arg(naive_result)} + lookup = {'%s__lt' % date_field: generic_view._make_date_lookup_arg(start)} ordering = '-%s' % date_field else: - lookup = {'%s__gte' % date_field: generic_view._make_date_lookup_arg(naive_result)} + lookup = {'%s__gte' % date_field: generic_view._make_date_lookup_arg(end)} ordering = date_field + # Filter out objects in the future if appropriate. + if not allow_future: + # Fortunately, to match the implementation of allow_future, + # we need __lte, which doesn't conflict with __lt above. + if generic_view.uses_datetime_field: + now = timezone.now() + else: + now = timezone_today() + lookup['%s__lte' % date_field] = now + qs = generic_view.get_queryset().filter(**lookup).order_by(ordering) # Snag the first object from the queryset; if it doesn't exist that @@ -640,26 +734,23 @@ def _get_next_prev(generic_view, naive_result, is_previous, period): try: result = getattr(qs[0], date_field) except IndexError: - result = None + return None - # Convert datetimes to a dates - if result and generic_view.uses_datetime_field: - if settings.USE_TZ: - result = timezone.localtime(result) - result = result.date() + # Convert datetimes to dates in the current time zone. + if generic_view.uses_datetime_field: + if settings.USE_TZ: + result = timezone.localtime(result) + result = result.date() - if result: - if period == 'month': - # first day of the month - result = result.replace(day=1) - elif period == 'week': - # monday of the week - result = result - datetime.timedelta(days=generic_view._get_weekday(result)) - elif period != 'day': - raise ValueError('invalid period: %s' % period) + # Return the first day of the period. + return get_current(result) - # Check against future dates. - if result and (allow_future or result < datetime.date.today()): - return result + +def timezone_today(): + """ + Return the current date in the current time zone. + """ + if settings.USE_TZ: + return timezone.localtime(timezone.now()).date() else: - return None + return datetime.date.today() diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index a2adb15d47..4b5281745e 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -1,6 +1,5 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.http import Http404 -from django.utils.encoding import smart_str from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateResponseMixin, ContextMixin, View @@ -81,7 +80,7 @@ class SingleObjectMixin(ContextMixin): if self.context_object_name: return self.context_object_name elif hasattr(obj, '_meta'): - return smart_str(obj._meta.object_name.lower()) + return obj._meta.object_name.lower() else: return None @@ -108,7 +107,7 @@ class SingleObjectTemplateResponseMixin(TemplateResponseMixin): def get_template_names(self): """ Return a list of template names to be used for the request. Must return - a list. May not be called if get_template is overridden. + a list. May not be called if render_to_response is overridden. """ try: names = super(SingleObjectTemplateResponseMixin, self).get_template_names() diff --git a/django/views/generic/list.py b/django/views/generic/list.py index d4664c34ef..ae45fd2218 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -1,7 +1,6 @@ from django.core.paginator import Paginator, InvalidPage from django.core.exceptions import ImproperlyConfigured from django.http import Http404 -from django.utils.encoding import smart_str from django.utils.translation import ugettext as _ from django.views.generic.base import TemplateResponseMixin, ContextMixin, View @@ -16,7 +15,7 @@ class MultipleObjectMixin(ContextMixin): def get_queryset(self): """ - Get the list of items for this view. This must be an interable, and may + Get the list of items for this view. This must be an iterable, and may be a queryset (in which qs-specific behavior will be enabled). """ if self.queryset is not None: @@ -77,7 +76,7 @@ class MultipleObjectMixin(ContextMixin): if self.context_object_name: return self.context_object_name elif hasattr(object_list, 'model'): - return smart_str('%s_list' % object_list.model._meta.object_name.lower()) + return '%s_list' % object_list.model._meta.object_name.lower() else: return None @@ -113,9 +112,19 @@ class BaseListView(MultipleObjectMixin, View): def get(self, request, *args, **kwargs): self.object_list = self.get_queryset() allow_empty = self.get_allow_empty() - if not allow_empty and len(self.object_list) == 0: - raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.") - % {'class_name': self.__class__.__name__}) + + if not allow_empty: + # When pagination is enabled and object_list is a queryset, + # it's better to do a cheap query than to load the unpaginated + # queryset in memory. + if (self.get_paginate_by(self.object_list) is not None + and hasattr(self.object_list, 'exists')): + is_empty = not self.object_list.exists() + else: + is_empty = len(self.object_list) == 0 + if is_empty: + raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.") + % {'class_name': self.__class__.__name__}) context = self.get_context_data(object_list=self.object_list) return self.render_to_response(context) @@ -126,7 +135,7 @@ class MultipleObjectTemplateResponseMixin(TemplateResponseMixin): def get_template_names(self): """ Return a list of template names to be used for the request. Must return - a list. May not be called if get_template is overridden. + a list. May not be called if render_to_response is overridden. """ try: names = super(MultipleObjectTemplateResponseMixin, self).get_template_names() diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index ba8765b253..4a27bdf7a9 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -61,7 +61,7 @@ look like this: poll.opened = False poll.save() - self.stdout.write('Successfully closed poll "%s"\n' % poll_id) + self.stdout.write('Successfully closed poll "%s"' % poll_id) .. note:: When you are using management commands and wish to provide console @@ -317,8 +317,12 @@ Exception class indicating a problem while executing a management command. If this exception is raised during the execution of a management -command, it will be caught and turned into a nicely-printed error -message to the appropriate output stream (i.e., stderr); as a -result, raising this exception (with a sensible description of the +command from a command line console, it will be caught and turned into a +nicely-printed error message to the appropriate output stream (i.e., stderr); +as a result, raising this exception (with a sensible description of the error) is the preferred way to indicate that something has gone wrong in the execution of a command. + +If a management command is called from code through +:ref:`call_command `, it's up to you to catch the exception +when needed. diff --git a/docs/intro/_images/admin10.png b/docs/intro/_images/admin10.png index d720bafc6a..fc30df9fb8 100644 Binary files a/docs/intro/_images/admin10.png and b/docs/intro/_images/admin10.png differ diff --git a/docs/intro/_images/admin11.png b/docs/intro/_images/admin11.png index 54412836f8..8576fcb883 100644 Binary files a/docs/intro/_images/admin11.png and b/docs/intro/_images/admin11.png differ diff --git a/docs/intro/_images/admin11t.png b/docs/intro/_images/admin11t.png index 7140265172..81accc9d49 100644 Binary files a/docs/intro/_images/admin11t.png and b/docs/intro/_images/admin11t.png differ diff --git a/docs/intro/_images/admin12.png b/docs/intro/_images/admin12.png index 622123da4a..2b4b1abc71 100644 Binary files a/docs/intro/_images/admin12.png and b/docs/intro/_images/admin12.png differ diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index d8258c00f8..250c0f1f41 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -13,9 +13,19 @@ It'll consist of two parts: * An admin site that lets you add, change and delete polls. We'll assume you have :doc:`Django installed ` already. You can -tell Django is installed by running the Python interactive interpreter and -typing ``import django``. If that command runs successfully, with no errors, -Django is installed. +tell Django is installed and which version by running the following command: + +.. code-block:: bash + + python -c "import django; print(django.get_version())" + +You should see either the version of your Django installation or an error +telling "No module named django". Check also that the version number matches +the version of this tutorial. If they don't match, you can refer to the +tutorial for your version of Django or update Django to the newest version. + +See :doc:`How to install Django ` for advice on how to remove +older versions of Django and install a newer one. .. admonition:: Where to get help: @@ -339,9 +349,10 @@ The first step in writing a database Web app in Django is to define your models the :ref:`DRY Principle `. The goal is to define your data model in one place and automatically derive things from it. -In our simple poll app, we'll create two models: polls and choices. A poll has -a question and a publication date. A choice has two fields: the text of the -choice and a vote tally. Each choice is associated with a poll. +In our simple poll app, we'll create two models: ``Poll`` and ``Choice``. +A ``Poll`` has a question and a publication date. A ``Choice`` has two fields: +the text of the choice and a vote tally. Each ``Choice`` is associated with a +``Poll``. These concepts are represented by simple Python classes. Edit the :file:`polls/models.py` file so it looks like this:: @@ -354,7 +365,7 @@ These concepts are represented by simple Python classes. Edit the class Choice(models.Model): poll = models.ForeignKey(Poll) - choice = models.CharField(max_length=200) + choice_text = models.CharField(max_length=200) votes = models.IntegerField() The code is straightforward. Each model is represented by a class that @@ -384,8 +395,8 @@ Some :class:`~django.db.models.Field` classes have required elements. schema, but in validation, as we'll soon see. Finally, note a relationship is defined, using -:class:`~django.db.models.ForeignKey`. That tells Django each Choice is related -to a single Poll. Django supports all the common database relationships: +:class:`~django.db.models.ForeignKey`. That tells Django each ``Choice`` is related +to a single ``Poll``. Django supports all the common database relationships: many-to-ones, many-to-manys and one-to-ones. .. _`Python path`: http://docs.python.org/tutorial/modules.html#the-module-search-path @@ -397,7 +408,7 @@ That small bit of model code gives Django a lot of information. With it, Django is able to: * Create a database schema (``CREATE TABLE`` statements) for this app. -* Create a Python database-access API for accessing Poll and Choice objects. +* Create a Python database-access API for accessing ``Poll`` and ``Choice`` objects. But first we need to tell our project that the ``polls`` app is installed. @@ -446,7 +457,7 @@ statements for the polls app): CREATE TABLE "polls_choice" ( "id" serial NOT NULL PRIMARY KEY, "poll_id" integer NOT NULL REFERENCES "polls_poll" ("id") DEFERRABLE INITIALLY DEFERRED, - "choice" varchar(200) NOT NULL, + "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL ); COMMIT; @@ -597,7 +608,7 @@ of this object. Let's fix that by editing the polls model (in the class Choice(models.Model): # ... def __unicode__(self): - return self.choice + return self.choice_text It's important to add :meth:`~django.db.models.Model.__unicode__` methods to your models, not only for your own sanity when dealing with the interactive @@ -678,7 +689,7 @@ Save these changes and start a new Python interactive shell by running True # Give the Poll a couple of Choices. The create call constructs a new - # choice object, does the INSERT statement, adds the choice to the set + # Choice object, does the INSERT statement, adds the choice to the set # of available choices and returns the new Choice object. Django creates # a set to hold the "other side" of a ForeignKey relation # (e.g. a poll's choices) which can be accessed via the API. @@ -689,11 +700,11 @@ Save these changes and start a new Python interactive shell by running [] # Create three choices. - >>> p.choice_set.create(choice='Not much', votes=0) + >>> p.choice_set.create(choice_text='Not much', votes=0) - >>> p.choice_set.create(choice='The sky', votes=0) + >>> p.choice_set.create(choice_text='The sky', votes=0) - >>> c = p.choice_set.create(choice='Just hacking again', votes=0) + >>> c = p.choice_set.create(choice_text='Just hacking again', votes=0) # Choice objects have API access to their related Poll objects. >>> c.poll @@ -713,7 +724,7 @@ Save these changes and start a new Python interactive shell by running [, , ] # Let's delete one of the choices. Use delete() for that. - >>> c = p.choice_set.filter(choice__startswith='Just hacking') + >>> c = p.choice_set.filter(choice_text__startswith='Just hacking') >>> c.delete() For more information on model relations, see :doc:`Accessing related objects diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index ee9e3d0d12..0d95f6ff37 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -276,11 +276,11 @@ in that window and click "Save," Django will save the poll to the database and dynamically add it as the selected choice on the "Add choice" form you're looking at. -But, really, this is an inefficient way of adding Choice objects to the system. +But, really, this is an inefficient way of adding ``Choice`` objects to the system. It'd be better if you could add a bunch of Choices directly when you create the -Poll object. Let's make that happen. +``Poll`` object. Let's make that happen. -Remove the ``register()`` call for the Choice model. Then, edit the ``Poll`` +Remove the ``register()`` call for the ``Choice`` model. Then, edit the ``Poll`` registration code to read:: class ChoiceInline(admin.StackedInline): @@ -296,7 +296,7 @@ registration code to read:: admin.site.register(Poll, PollAdmin) -This tells Django: "Choice objects are edited on the Poll admin page. By +This tells Django: "``Choice`` objects are edited on the ``Poll`` admin page. By default, provide enough fields for 3 choices." Load the "Add poll" page to see how that looks, you may need to restart your development server: @@ -309,7 +309,7 @@ by ``extra`` -- and each time you come back to the "Change" page for an already-created object, you get another three extra slots. One small problem, though. It takes a lot of screen space to display all the -fields for entering related Choice objects. For that reason, Django offers a +fields for entering related ``Choice`` objects. For that reason, Django offers a tabular way of displaying inline related objects; you just need to change the ``ChoiceInline`` declaration to read:: @@ -397,7 +397,7 @@ search terms, Django will search the ``question`` field. You can use as many fields as you'd like -- although because it uses a ``LIKE`` query behind the scenes, keep it reasonable, to keep your database happy. -Finally, because Poll objects have dates, it'd be convenient to be able to +Finally, because ``Poll`` objects have dates, it'd be convenient to be able to drill down by date. Add this line:: date_hierarchy = 'pub_date' diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index c45c944acb..fd3a04ba93 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -400,7 +400,7 @@ like:

    {{ poll.question }}

      {% for choice in poll.choice_set.all %} -
    • {{ choice.choice }}
    • +
    • {{ choice.choice_text }}
    • {% endfor %}
    @@ -412,7 +412,7 @@ list-index lookup. Method-calling happens in the :ttag:`{% for %}` loop: ``poll.choice_set.all`` is interpreted as the Python code -``poll.choice_set.all()``, which returns an iterable of Choice objects and is +``poll.choice_set.all()``, which returns an iterable of ``Choice`` objects and is suitable for use in the :ttag:`{% for %}` tag. See the :doc:`template guide ` for more about templates. diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 8a9f4ee380..fac1433a8d 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -22,7 +22,7 @@ tutorial, so that the template contains an HTML ``
    `` element: {% csrf_token %} {% for choice in poll.choice_set.all %} -
    +
    {% endfor %} @@ -168,7 +168,7 @@ Now, create a ``results.html`` template:
      {% for choice in poll.choice_set.all %} -
    • {{ choice.choice }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}
    • +
    • {{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}
    • {% endfor %}
    diff --git a/docs/ref/class-based-views.txt b/docs/ref/class-based-views.txt index 174539d162..acd9db2d66 100644 --- a/docs/ref/class-based-views.txt +++ b/docs/ref/class-based-views.txt @@ -1171,7 +1171,15 @@ YearArchiveView have objects available according to ``queryset``, represented as ``datetime.datetime`` objects, in ascending order. - * ``year``: The given year, as a four-character string. + * ``year``: A ``datetime.date`` object representing the given year. + + * ``next_year``: A ``datetime.date`` object representing the first day + of the next year. If the next year is in the future, this will be + ``None``. + + * ``previous_year``: A ``datetime.date`` object representing the first + day of the previous year. Unlike ``next_year``, this will never be + ``None``. **Notes** @@ -1255,6 +1263,14 @@ WeekArchiveView * ``week``: A ``datetime.date`` object representing the first day of the given week. + * ``next_week``: A ``datetime.date`` object representing the first day + of the next week. If the next week is in the future, this will be + ``None``. + + * ``previous_week``: A ``datetime.date`` object representing the first + day of the previous week. Unlike ``next_week``, this will never be + ``None``. + **Notes** * Uses a default ``template_name_suffix`` of ``_archive_week``. diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index a3d9673db9..7aafbe89f3 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -186,6 +186,7 @@ Here's a full example template: .. code-block:: html+django {% extends "base.html" %} + {% load i18n %} {% block head %} {{ wizard.form.media }} diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 7501fb3ec8..b9e3a7acd3 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -941,7 +941,7 @@ of the geometry field in each model converted to the requested output format. ``geohash`` ~~~~~~~~~~~ -.. method:: GeoQuerySet.geohash(preceision=20, **kwargs) +.. method:: GeoQuerySet.geohash(precision=20, **kwargs) .. versionadded:: 1.2 diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 1dbd00b299..2bdf825316 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -348,6 +348,15 @@ CachedStaticFilesStorage :setting:`CACHES` setting named ``'staticfiles'``. It falls back to using the ``'default'`` cache backend. + .. method:: file_hash(name, content=None) + + .. versionadded:: 1.5 + + The method that is used when creating the hashed name of a file. + Needs to return a hash for the given file name and content. + By default it calculates a MD5 hash from the content's chunks as + mentioned above. + .. _`far future Expires headers`: http://developer.yahoo.com/performance/rules.html#expires .. _`@import`: http://www.w3.org/TR/CSS2/cascade.html#at-import .. _`url()`: http://www.w3.org/TR/CSS2/syndata.html#uri diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt index 013d113c84..10108d1f4f 100644 --- a/docs/ref/files/file.txt +++ b/docs/ref/files/file.txt @@ -96,7 +96,7 @@ The ``ContentFile`` Class from django.core.files.base import ContentFile - f1 = ContentFile("my string content") + f1 = ContentFile(b"my string content") f2 = ContentFile(u"my unicode content encoded as UTF-8".encode('UTF-8')) .. currentmodule:: django.core.files.images diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 398c90661b..a1b76f65e1 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1605,7 +1605,8 @@ method. This takes some explanation. By default, ``is_secure()`` is able to determine whether a request is secure by looking at whether the requested URL uses -"https://". +"https://". This is important for Django's CSRF protection, and may be used +by your own code or third-party apps. If your Django app is behind a proxy, though, the proxy may be "swallowing" the fact that a request is HTTPS, using a non-HTTPS connection between the proxy @@ -1635,7 +1636,7 @@ available in ``request.META``.) .. warning:: - **You will probably open security holes in your site if you set this without knowing what you're doing. Seriously.** + **You will probably open security holes in your site if you set this without knowing what you're doing. And if you fail to set it when you should. Seriously.** Make sure ALL of the following are true before setting this (assuming the values from the example above): diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index aece572e07..e945e0d4ca 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -790,9 +790,10 @@ templating functions, call :func:`django.conf.settings.configure()` with any settings you wish to specify. You might want to consider setting at least :setting:`TEMPLATE_DIRS` (if you're going to use template loaders), :setting:`DEFAULT_CHARSET` (although the default of ``utf-8`` is probably fine) -and :setting:`TEMPLATE_DEBUG`. All available settings are described in the -:doc:`settings documentation `, and any setting starting with -``TEMPLATE_`` is of obvious interest. +and :setting:`TEMPLATE_DEBUG`. If you plan to use the :ttag:`url` template tag, +you will also need to set the :setting:`ROOT_URLCONF` setting. All available +settings are described in the :doc:`settings documentation `, +and any setting starting with ``TEMPLATE_`` is of obvious interest. .. _topic-template-alternate-language: diff --git a/docs/ref/unicode.txt b/docs/ref/unicode.txt index 1286dcfdd0..46ce4138a4 100644 --- a/docs/ref/unicode.txt +++ b/docs/ref/unicode.txt @@ -269,7 +269,7 @@ You can pass either Unicode strings or UTF-8 bytestrings as arguments to querysets are identical:: qs = People.objects.filter(name__contains=u'Å') - qs = People.objects.filter(name__contains='\xc3\x85') # UTF-8 encoding of Å + qs = People.objects.filter(name__contains=b'\xc3\x85') # UTF-8 encoding of Å Templates ========= @@ -277,7 +277,7 @@ Templates You can use either Unicode or bytestrings when creating templates manually:: from django.template import Template - t1 = Template('This is a bytestring template.') + t1 = Template(b'This is a bytestring template.') t2 = Template(u'This is a Unicode template.') But the common case is to read templates from the filesystem, and this creates diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index be06fe58d8..0d86a52670 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -44,6 +44,24 @@ reasons or when trying to avoid overwriting concurrent changes. See the :meth:`Model.save() ` documentation for more details. +Caching of related model instances +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When traversing relations, the ORM will avoid re-fetching objects that were +previously loaded. For example, with the tutorial's models:: + + >>> first_poll = Poll.objects.all()[0] + >>> first_choice = first_poll.choice_set.all()[0] + >>> first_choice.poll is first_poll + True + +In Django 1.5, the third line no longer triggers a new SQL query to fetch +``first_choice.poll``; it was set by the second line. + +For one-to-one relationships, both sides can be cached. For many-to-one +relationships, only the single side of the relationship can be cached. This +is particularly helpful in combination with ``prefetch_related``. + Minor features ~~~~~~~~~~~~~~ @@ -55,6 +73,15 @@ Django 1.5 also includes several smaller improvements worth noting: * :mod:`django.utils.timezone` provides a helper for converting aware datetimes between time zones. See :func:`~django.utils.timezone.localtime`. +* The generic views support OPTIONS requests. + +* Management commands do not raise ``SystemExit`` any more when called by code + from :ref:`call_command `. Any exception raised by the command + (mostly :ref:`CommandError `) is propagated. + +* The dumpdata management command outputs one row at a time, preventing + out-of-memory errors when dumping large datasets. + * In the localflavor for Canada, "pq" was added to the acceptable codes for Quebec. It's an old abbreviation. @@ -69,6 +96,38 @@ Backwards incompatible changes in 1.5 deprecation timeline for a given feature, its removal may appear as a backwards incompatible change. +Context in year archive class-based views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For consistency with the other date-based generic views, +:class:`~django.views.generic.dates.YearArchiveView` now passes ``year`` in +the context as a :class:`datetime.date` rather than a string. If you are +using ``{{ year }}`` in your templates, you must replace it with ``{{ +year|date:"Y" }}``. + +``next_year`` and ``previous_year`` were also added in the context. They are +calculated according to ``allow_empty`` and ``allow_future``. + +OPTIONS, PUT and DELETE requests in the test client +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unlike GET and POST, these HTTP methods aren't implemented by web browsers. +Rather, they're used in APIs, which transfer data in various formats such as +JSON or XML. Since such requests may contain arbitrary data, Django doesn't +attempt to decode their body. + +However, the test client used to build a query string for OPTIONS and DELETE +requests like for GET, and a request body for PUT requests like for POST. This +encoding was arbitrary and inconsistent with Django's behavior when it +receives the requests, so it was removed in Django 1.5. + +If you were using the ``data`` parameter in an OPTIONS or a DELETE request, +you must convert it to a query string and append it to the ``path`` parameter. + +If you were using the ``data`` parameter in a PUT request without a +``content_type``, you must encode your data before passing it to the test +client and set the ``content_type`` argument. + Features deprecated in 1.5 ========================== @@ -84,4 +143,4 @@ our own copy of ``simplejson``. You can safely change any use of ~~~~~~~~~~~~~~~~~~~~~~ The :func:`~django.utils.itercompat.product` function has been deprecated. Use -the built-in `itertools.product` instead. +the built-in :func:`itertools.product` instead. diff --git a/docs/topics/class-based-views.txt b/docs/topics/class-based-views.txt index abac22000f..13c2b994e4 100644 --- a/docs/topics/class-based-views.txt +++ b/docs/topics/class-based-views.txt @@ -35,9 +35,6 @@ Django ships with generic views to do the following: * Present date-based objects in year/month/day archive pages, associated detail, and "latest" pages. - `The Django Weblog `_'s - year, month, and day archives are built with these, as would be a typical - newspaper's archives. * Allow users to create, update, and delete objects -- with or without authorization. diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index ac45c50aa8..42b9d1f490 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -152,6 +152,8 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the forms in the formset. Validation was performed for each of the two forms, and the expected error message appears for the second item. +.. versionadded:: 1.4 + We can also check if form data differs from the initial data (i.e. the form was sent without any data):: diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 18e55f5eb6..4870ffc094 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -99,7 +99,7 @@ The standard pattern for processing a form in a view looks like this: else: form = ContactForm() # An unbound form - return render_to_response('contact.html', { + return render(request, 'contact.html', { 'form': form, }) diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index 89d5ef217e..1d9dd4b3c6 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -656,7 +656,7 @@ Usage >>> from django.utils.dateparse import parse_datetime >>> naive = parse_datetime("2012-02-21 10:28:45") >>> import pytz - >>> pytz.timezone("Europe/Helsinki").localize(naive) + >>> pytz.timezone("Europe/Helsinki").localize(naive, is_dst=None) datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=) Note that ``localize`` is a pytz extension to the :class:`~datetime.tzinfo` diff --git a/docs/topics/install.txt b/docs/topics/install.txt index 4fd745d22b..5e2eefe139 100644 --- a/docs/topics/install.txt +++ b/docs/topics/install.txt @@ -154,34 +154,19 @@ If you are upgrading your installation of Django from a previous version, you will need to uninstall the old Django version before installing the new version. -If you installed Django using ``setup.py install``, uninstalling -is as simple as deleting the ``django`` directory from your Python -``site-packages``. +If you installed Django using pip_ or ``easy_install`` previously, installing +with pip_ or ``easy_install`` again will automatically take care of the old +version, so you don't need to do it yourself. -If you installed Django from a Python egg, remove the Django ``.egg`` file, -and remove the reference to the egg in the file named ``easy-install.pth``. -This file should also be located in your ``site-packages`` directory. +If you previously installed Django using ``python setup.py install``, +uninstalling is as simple as deleting the ``django`` directory from your Python +``site-packages``. To find the directory you need to remove, you can run the +following at your shell prompt (not the interactive Python prompt): -.. _finding-site-packages: +.. code-block:: bash -.. admonition:: Where are my ``site-packages`` stored? + python -c "import sys; sys.path = sys.path[1:]; import django; print(django.__path__)" - The location of the ``site-packages`` directory depends on the operating - system, and the location in which Python was installed. To find out your - system's ``site-packages`` location, execute the following: - - .. code-block:: bash - - python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())" - - (Note that this should be run from a shell prompt, not a Python interactive - prompt.) - - Some Debian-based Linux distributions have separate ``site-packages`` - directories for user-installed packages, such as when installing Django - from a downloaded tarball. The command listed above will give you the - system's ``site-packages``, the user's directory can be found in - ``/usr/local/lib/`` instead of ``/usr/lib/``. .. _install-django-code: @@ -253,6 +238,15 @@ Installing an official release manually run the command ``python setup.py install``. This will install Django in your Python installation's ``site-packages`` directory. + .. admonition:: Removing an old version + + If you use this installation technique, it is particularly important + that you :ref:`remove any existing + installations` of Django + first. Otherwise, you can end up with a broken installation that + includes files from previous versions that have since been removed from + Django. + .. _download page: https://www.djangoproject.com/download/ .. _bsdtar: http://gnuwin32.sourceforge.net/packages/bsdtar.htm .. _7-zip: http://www.7-zip.org/ @@ -291,44 +285,26 @@ latest bug fixes and improvements, follow these instructions: This will create a directory ``django-trunk`` in your current directory. -3. Next, make sure that the Python interpreter can load Django's code. The most - convenient way to do this is to `modify Python's search path`_. Add a ``.pth`` - file containing the full path to the ``django-trunk`` directory to your - system's ``site-packages`` directory. For example, on a Unix-like system: +3. Make sure that the Python interpreter can load Django's code. The most + convenient way to do this is via pip_. Run the following command: .. code-block:: bash - echo WORKING-DIR/django-trunk > SITE-PACKAGES-DIR/django.pth + sudo pip install -e django-trunk/ - (In the above line, change ``SITE-PACKAGES-DIR`` to match the location of - your system's ``site-packages`` directory, as explained in the - :ref:`Where are my site-packages stored? ` section - above. Change ``WORKING-DIR/django-trunk`` to match the full path to your - new ``django-trunk`` directory.) + (If using a virtualenv_ you can omit ``sudo``.) -4. On Unix-like systems, create a symbolic link to the file - ``django-trunk/django/bin/django-admin.py`` in a directory on your system - path, such as ``/usr/local/bin``. For example: + This will make Django's code importable, and will also make the + ``django-admin.py`` utility command available. In other words, you're all + set! - .. code-block:: bash - - ln -s WORKING-DIR/django-trunk/django/bin/django-admin.py /usr/local/bin/ - - (In the above line, change WORKING-DIR to match the full path to your new - ``django-trunk`` directory.) - - This simply lets you type ``django-admin.py`` from within any directory, - rather than having to qualify the command with the full path to the file. - - On Windows systems, the same result can be achieved by copying the file - ``django-trunk/django/bin/django-admin.py`` to somewhere on your system - path, for example ``C:\Python27\Scripts``. + If you don't have pip_ available, see the alternative instructions for + `installing the development version without pip`_. .. warning:: Don't run ``sudo python setup.py install``, because you've already - carried out the equivalent actions in steps 3 and 4. Furthermore, this is - known to cause problems when updating to a more recent version of Django. + carried out the equivalent actions in step 3. When you want to update your copy of the Django source code, just run the command ``git pull`` from within the ``django-trunk`` directory. When you do @@ -336,3 +312,61 @@ this, Git will automatically download any changes. .. _Git: http://git-scm.com/ .. _`modify Python's search path`: http://docs.python.org/install/index.html#modifying-python-s-search-path +.. _installing-the-development-version-without-pip: + +Installing the development version without pip +---------------------------------------------- + +If you don't have pip_, you can instead manually `modify Python's search +path`_. + +First follow steps 1 and 2 above, so that you have a ``django-trunk`` directory +with a checkout of Django's latest code in it. Then add a ``.pth`` file +containing the full path to the ``django-trunk`` directory to your system's +``site-packages`` directory. For example, on a Unix-like system: + +.. code-block:: bash + + echo WORKING-DIR/django-trunk > SITE-PACKAGES-DIR/django.pth + +In the above line, change ``WORKING-DIR/django-trunk`` to match the full path +to your new ``django-trunk`` directory, and change ``SITE-PACKAGES-DIR`` to +match the location of your system's ``site-packages`` directory. + +The location of the ``site-packages`` directory depends on the operating +system, and the location in which Python was installed. To find your system's +``site-packages`` location, execute the following: + +.. code-block:: bash + + python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())" + +(Note that this should be run from a shell prompt, not a Python interactive +prompt.) + +Some Debian-based Linux distributions have separate ``site-packages`` +directories for user-installed packages, such as when installing Django from +a downloaded tarball. The command listed above will give you the system's +``site-packages``, the user's directory can be found in ``/usr/local/lib/`` +instead of ``/usr/lib/``. + +Next you need to make the ``django-admin.py`` utility available in your +shell PATH. + +On Unix-like systems, create a symbolic link to the file +``django-trunk/django/bin/django-admin.py`` in a directory on your system +path, such as ``/usr/local/bin``. For example: + +.. code-block:: bash + + ln -s WORKING-DIR/django-trunk/django/bin/django-admin.py /usr/local/bin/ + +(In the above line, change WORKING-DIR to match the full path to your new +``django-trunk`` directory.) + +This simply lets you type ``django-admin.py`` from within any directory, +rather than having to qualify the command with the full path to the file. + +On Windows systems, the same result can be achieved by copying the file +``django-trunk/django/bin/django-admin.py`` to somewhere on your system +path, for example ``C:\Python27\Scripts``. diff --git a/docs/topics/security.txt b/docs/topics/security.txt index 193d0029a4..151853d4ac 100644 --- a/docs/topics/security.txt +++ b/docs/topics/security.txt @@ -122,22 +122,19 @@ transferred between client and server, and in some cases -- **active** network attackers -- to alter data that is sent in either direction. If you want the protection that HTTPS provides, and have enabled it on your -server, there are some additional steps to consider to ensure that sensitive -information is not leaked: +server, there are some additional steps you may need: + +* If necessary, set :setting:`SECURE_PROXY_SSL_HEADER`, ensuring that you have + understood the warnings there thoroughly. Failure to do this can result + in CSRF vulnerabilities, and failure to do it correctly can also be + dangerous! * Set up redirection so that requests over HTTP are redirected to HTTPS. - It is possible to do this with a piece of Django middleware. However, this has - problems for the common case of a Django app running behind a reverse - proxy. Often, reverse proxies are configured to set the ``X-Forwarded-SSL`` - header (or equivalent) if the incoming connection was HTTPS, and the absence - of this header could be used to detect a request that was not HTTPS. However, - this method usually cannot be relied on, as a client, or a malicious active - network attacker, could also set this header. - - So, for the case of a reverse proxy, it is recommended that the main Web - server should be configured to do the redirect to HTTPS, or configured to send - HTTP requests to an app that unconditionally redirects to HTTPS. + This could be done using a custom middleware. Please note the caveats under + :setting:`SECURE_PROXY_SSL_HEADER`. For the case of a reverse proxy, it may be + easier or more secure to configure the main Web server to do the redirect to + HTTPS. * Use 'secure' cookies. diff --git a/docs/topics/settings.txt b/docs/topics/settings.txt index 52cc06da56..88fa7b6864 100644 --- a/docs/topics/settings.txt +++ b/docs/topics/settings.txt @@ -220,7 +220,7 @@ In this example, default settings are taken from ``myapp_defaults``, and the The following example, which uses ``myapp_defaults`` as a positional argument, is equivalent:: - settings.configure(myapp_defaults, DEBUG = True) + settings.configure(myapp_defaults, DEBUG=True) Normally, you will not need to override the defaults in this fashion. The Django defaults are sufficiently tame that you can safely use them. Be aware @@ -242,7 +242,16 @@ is accessed. If you set ``DJANGO_SETTINGS_MODULE``, access settings values somehow, *then* call ``configure()``, Django will raise a ``RuntimeError`` indicating -that settings have already been configured. +that settings have already been configured. There is a property just for this +purpose: + +.. attribute: django.conf.settings.configured + +For example:: + + from django.conf import settings + if not settings.configured: + settings.configure(myapp_defaults, DEBUG=True) Also, it's an error to call ``configure()`` more than once, or to call ``configure()`` after any setting has been accessed. diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index d5ccc2d8fc..f35c545c30 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -805,45 +805,56 @@ arguments at time of construction: .. method:: Client.head(path, data={}, follow=False, **extra) - Makes a HEAD request on the provided ``path`` and returns a ``Response`` - object. Useful for testing RESTful interfaces. Acts just like - :meth:`Client.get` except it does not return a message body. + Makes a HEAD request on the provided ``path`` and returns a + ``Response`` object. This method works just like :meth:`Client.get`, + including the ``follow`` and ``extra`` arguments, except it does not + return a message body. - If you set ``follow`` to ``True`` the client will follow any redirects - and a ``redirect_chain`` attribute will be set in the response object - containing tuples of the intermediate urls and status codes. - - .. method:: Client.options(path, data={}, follow=False, **extra) + .. method:: Client.options(path, data='', content_type='application/octet-stream', follow=False, **extra) Makes an OPTIONS request on the provided ``path`` and returns a ``Response`` object. Useful for testing RESTful interfaces. - If you set ``follow`` to ``True`` the client will follow any redirects - and a ``redirect_chain`` attribute will be set in the response object - containing tuples of the intermediate urls and status codes. + When ``data`` is provided, it is used as the request body, and + a ``Content-Type`` header is set to ``content_type``. - The ``extra`` argument acts the same as for :meth:`Client.get`. + .. versionchanged:: 1.5 + :meth:`Client.options` used to process ``data`` like + :meth:`Client.get`. - .. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT, follow=False, **extra) + The ``follow`` and ``extra`` arguments act the same as for + :meth:`Client.get`. + + .. method:: Client.put(path, data='', content_type='application/octet-stream', follow=False, **extra) Makes a PUT request on the provided ``path`` and returns a - ``Response`` object. Useful for testing RESTful interfaces. Acts just - like :meth:`Client.post` except with the PUT request method. + ``Response`` object. Useful for testing RESTful interfaces. - If you set ``follow`` to ``True`` the client will follow any redirects - and a ``redirect_chain`` attribute will be set in the response object - containing tuples of the intermediate urls and status codes. + When ``data`` is provided, it is used as the request body, and + a ``Content-Type`` header is set to ``content_type``. - .. method:: Client.delete(path, follow=False, **extra) + .. versionchanged:: 1.5 + :meth:`Client.put` used to process ``data`` like + :meth:`Client.post`. + + The ``follow`` and ``extra`` arguments act the same as for + :meth:`Client.get`. + + .. method:: Client.delete(path, data='', content_type='application/octet-stream', follow=False, **extra) Makes an DELETE request on the provided ``path`` and returns a ``Response`` object. Useful for testing RESTful interfaces. - If you set ``follow`` to ``True`` the client will follow any redirects - and a ``redirect_chain`` attribute will be set in the response object - containing tuples of the intermediate urls and status codes. + When ``data`` is provided, it is used as the request body, and + a ``Content-Type`` header is set to ``content_type``. + + .. versionchanged:: 1.5 + :meth:`Client.delete` used to process ``data`` like + :meth:`Client.get`. + + The ``follow`` and ``extra`` arguments act the same as for + :meth:`Client.get`. - The ``extra`` argument acts the same as for :meth:`Client.get`. .. method:: Client.login(**credentials) diff --git a/setup.py b/setup.py index a19f660a5a..165c5e9f73 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,25 @@ from distutils.core import setup from distutils.command.install_data import install_data from distutils.command.install import INSTALL_SCHEMES +from distutils.sysconfig import get_python_lib import os import sys +# Warn if we are installing over top of an existing installation. This can +# cause issues where files that were deleted from a more recent Django are +# still present in site-packages. See #18115. +overlay_warning = False +if "install" in sys.argv: + # We have to try also with an explicit prefix of /usr/local in order to + # catch Debian's custom user site-packages directory. + for lib_path in get_python_lib(), get_python_lib(prefix="/usr/local"): + existing_path = os.path.abspath(os.path.join(lib_path, "django")) + if os.path.exists(existing_path): + # We note the need for the warning here, but present it after the + # command is run, so it's more likely to be seen. + overlay_warning = True + break + class osx_install_data(install_data): # On MacOS, the platform-specific lib dir is /System/Library/Framework/Python/.../ # which is wrong. Python 2.5 supplied with MacOS 10.5 has an Apple-specific fix @@ -97,3 +113,23 @@ setup( 'Topic :: Software Development :: Libraries :: Python Modules', ], ) + +if overlay_warning: + sys.stderr.write(""" + +======== +WARNING! +======== + +You have just installed Django over top of an existing +installation, without removing it first. Because of this, +your install may now include extraneous files from a +previous version that have since been removed from +Django. This is known to cause a variety of problems. You +should manually remove the + +%(existing_path)s + +directory and re-install Django. + +""" % { "existing_path": existing_path }) diff --git a/tests/modeltests/defer/tests.py b/tests/modeltests/defer/tests.py index 09138293af..eb09162b01 100644 --- a/tests/modeltests/defer/tests.py +++ b/tests/modeltests/defer/tests.py @@ -158,3 +158,18 @@ class DeferTests(TestCase): self.assert_delayed(child, 1) self.assertEqual(child.name, 'p1') self.assertEqual(child.value, 'xx') + + def test_defer_inheritance_pk_chaining(self): + """ + When an inherited model is fetched from the DB, its PK is also fetched. + When getting the PK of the parent model it is useful to use the already + fetched parent model PK if it happens to be available. Tests that this + is done. + """ + s1 = Secondary.objects.create(first="x1", second="y1") + bc = BigChild.objects.create(name="b1", value="foo", related=s1, + other="bar") + bc_deferred = BigChild.objects.only('name').get(pk=bc.pk) + with self.assertNumQueries(0): + bc_deferred.id + self.assertEqual(bc_deferred.pk, bc_deferred.id) diff --git a/tests/modeltests/field_subclassing/tests.py b/tests/modeltests/field_subclassing/tests.py index c6a2335f37..48755123f2 100644 --- a/tests/modeltests/field_subclassing/tests.py +++ b/tests/modeltests/field_subclassing/tests.py @@ -17,6 +17,10 @@ class CustomField(TestCase): self.assertTrue(isinstance(d.data, list)) self.assertEqual(d.data, [1, 2, 3]) + d = DataModel.objects.defer("data").get(pk=d.pk) + self.assertTrue(isinstance(d.data, list)) + self.assertEqual(d.data, [1, 2, 3]) + # Refetch for save d = DataModel.objects.defer("data").get(pk=d.pk) d.save() diff --git a/tests/modeltests/files/tests.py b/tests/modeltests/files/tests.py index 3bfe193610..837723a049 100644 --- a/tests/modeltests/files/tests.py +++ b/tests/modeltests/files/tests.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import gzip import shutil import tempfile @@ -8,11 +9,12 @@ from django.core.files import File from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase +from django.utils import unittest from .models import Storage, temp_storage, temp_storage_location -class FileTests(TestCase): +class FileStorageTests(TestCase): def tearDown(self): shutil.rmtree(temp_storage_location) @@ -106,6 +108,8 @@ class FileTests(TestCase): obj3.default.delete() obj4.random.delete() + +class FileTests(unittest.TestCase): def test_context_manager(self): orig_file = tempfile.TemporaryFile() base_file = File(orig_file) @@ -114,3 +118,10 @@ class FileTests(TestCase): self.assertFalse(f.closed) self.assertTrue(f.closed) self.assertTrue(orig_file.closed) + + def test_file_mode(self): + # Should not set mode to None if it is not present. + # See #14681, stdlib gzip module crashes if mode is set to None + file = SimpleUploadedFile("mode_test.txt", "content") + self.assertFalse(hasattr(file, 'mode')) + g = gzip.GzipFile(fileobj=file) diff --git a/tests/modeltests/fixtures/tests.py b/tests/modeltests/fixtures/tests.py index d22010d7a0..478bbe9129 100644 --- a/tests/modeltests/fixtures/tests.py +++ b/tests/modeltests/fixtures/tests.py @@ -54,25 +54,25 @@ class FixtureLoadingTests(TestCase): ]) # Dump the current contents of the database as a JSON fixture - self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') # Try just dumping the contents of fixtures.Category self._dumpdata_assert(['fixtures.Category'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}]') # ...and just fixtures.Article - self._dumpdata_assert(['fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}]') + self._dumpdata_assert(['fixtures.Article'], '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}]') # ...and both - self._dumpdata_assert(['fixtures.Category', 'fixtures.Article'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}]') + self._dumpdata_assert(['fixtures.Category', 'fixtures.Article'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}]') # Specify a specific model twice - self._dumpdata_assert(['fixtures.Article', 'fixtures.Article'], '[{"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}]') + self._dumpdata_assert(['fixtures.Article', 'fixtures.Article'], '[{"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}]') # Specify a dump that specifies Article both explicitly and implicitly - self._dumpdata_assert(['fixtures.Article', 'fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') + self._dumpdata_assert(['fixtures.Article', 'fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') # Same again, but specify in the reverse order - self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') # Specify one model from one application, and an entire other application. self._dumpdata_assert(['fixtures.Category', 'sites'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 1, "model": "sites.site", "fields": {"domain": "example.com", "name": "example.com"}}]') @@ -143,17 +143,17 @@ class FixtureLoadingTests(TestCase): ]) # By default, you get raw keys on dumpdata - self._dumpdata_assert(['fixtures.book'], '[{"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [3, 1]}}]') + self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [3, 1]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') # But you can get natural keys if you ask for them and they are available - self._dumpdata_assert(['fixtures.book'], '[{"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}]', natural_keys=True) + self._dumpdata_assert(['fixtures.book'], '[{"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) # Dump the current contents of the database as a JSON fixture - self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 5, "model": "fixtures.article", "fields": {"headline": "XML identified as leading cause of cancer", "pub_date": "2006-06-16T16:00:00"}}, {"pk": 4, "model": "fixtures.article", "fields": {"headline": "Django conquers world!", "pub_date": "2006-06-16T15:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker on TV is great!", "pub_date": "2006-06-16T11:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "legal", "tagged_id": 3}}, {"pk": 3, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "django", "tagged_id": 4}}, {"pk": 4, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "world domination", "tagged_id": 4}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Artist formerly known as \\"Prince\\""}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 1, "model": "fixtures.visa", "fields": {"person": ["Django Reinhardt"], "permissions": [["add_user", "auth", "user"], ["change_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 2, "model": "fixtures.visa", "fields": {"person": ["Stephane Grappelli"], "permissions": [["add_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 3, "model": "fixtures.visa", "fields": {"person": ["Artist formerly known as \\"Prince\\""], "permissions": [["change_user", "auth", "user"]]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}]', natural_keys=True) + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker on TV is great!", "pub_date": "2006-06-16T11:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}, {"pk": 4, "model": "fixtures.article", "fields": {"headline": "Django conquers world!", "pub_date": "2006-06-16T15:00:00"}}, {"pk": 5, "model": "fixtures.article", "fields": {"headline": "XML identified as leading cause of cancer", "pub_date": "2006-06-16T16:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "legal", "tagged_id": 3}}, {"pk": 3, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "django", "tagged_id": 4}}, {"pk": 4, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "world domination", "tagged_id": 4}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Artist formerly known as \\"Prince\\""}}, {"pk": 1, "model": "fixtures.visa", "fields": {"person": ["Django Reinhardt"], "permissions": [["add_user", "auth", "user"], ["change_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 2, "model": "fixtures.visa", "fields": {"person": ["Stephane Grappelli"], "permissions": [["add_user", "auth", "user"], ["delete_user", "auth", "user"]]}}, {"pk": 3, "model": "fixtures.visa", "fields": {"person": ["Artist formerly known as \\"Prince\\""], "permissions": [["change_user", "auth", "user"]]}}, {"pk": 1, "model": "fixtures.book", "fields": {"name": "Music for all ages", "authors": [["Artist formerly known as \\"Prince\\""], ["Django Reinhardt"]]}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) # Dump the current contents of the database as an XML fixture self._dumpdata_assert(['fixtures'], """ -News StoriesLatest news storiesXML identified as leading cause of cancer2006-06-16T16:00:00Django conquers world!2006-06-16T15:00:00Copyright is fine the way it is2006-06-16T14:00:00Poker on TV is great!2006-06-16T11:00:00copyrightfixturesarticle3legalfixturesarticle3djangofixturesarticle4world dominationfixturesarticle4Artist formerly known as "Prince"Django ReinhardtStephane GrappelliDjango Reinhardtadd_userauthuserchange_userauthuserdelete_userauthuserStephane Grappelliadd_userauthuserdelete_userauthuserArtist formerly known as "Prince"change_userauthuserAchieving self-awareness of Python programsMusic for all agesArtist formerly known as "Prince"Django Reinhardt""", format='xml', natural_keys=True) +News StoriesLatest news storiesPoker on TV is great!2006-06-16T11:00:00Copyright is fine the way it is2006-06-16T14:00:00Django conquers world!2006-06-16T15:00:00XML identified as leading cause of cancer2006-06-16T16:00:00copyrightfixturesarticle3legalfixturesarticle3djangofixturesarticle4world dominationfixturesarticle4Django ReinhardtStephane GrappelliArtist formerly known as "Prince"Django Reinhardtadd_userauthuserchange_userauthuserdelete_userauthuserStephane Grappelliadd_userauthuserdelete_userauthuserArtist formerly known as "Prince"change_userauthuserMusic for all agesArtist formerly known as "Prince"Django ReinhardtAchieving self-awareness of Python programs""", format='xml', natural_keys=True) def test_dumpdata_with_excludes(self): # Load fixture1 which has a site, two articles, and a category @@ -185,14 +185,14 @@ class FixtureLoadingTests(TestCase): exclude_list=['fixtures.Article', 'fixtures.Book', 'sites']) # Excluding a bogus app should throw an error - self.assertRaises(SystemExit, + self.assertRaises(management.CommandError, self._dumpdata_assert, ['fixtures', 'sites'], '', exclude_list=['foo_app']) # Excluding a bogus model should throw an error - self.assertRaises(SystemExit, + self.assertRaises(management.CommandError, self._dumpdata_assert, ['fixtures', 'sites'], '', @@ -292,11 +292,11 @@ class FixtureLoadingTests(TestCase): ]) # Dump the current contents of the database as a JSON fixture - self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 1, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "copyright", "tagged_id": 3}}, {"pk": 2, "model": "fixtures.tag", "fields": {"tagged_type": ["fixtures", "article"], "name": "law", "tagged_id": 3}}, {"pk": 1, "model": "fixtures.person", "fields": {"name": "Django Reinhardt"}}, {"pk": 2, "model": "fixtures.person", "fields": {"name": "Stephane Grappelli"}}, {"pk": 3, "model": "fixtures.person", "fields": {"name": "Prince"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]', natural_keys=True) # Dump the current contents of the database as an XML fixture self._dumpdata_assert(['fixtures'], """ -News StoriesLatest news storiesTime to reform copyright2006-06-16T13:00:00Poker has no place on ESPN2006-06-16T12:00:00copyrightfixturesarticle3lawfixturesarticle3Django ReinhardtPrinceStephane GrappelliAchieving self-awareness of Python programs""", format='xml', natural_keys=True) +News StoriesLatest news storiesPoker has no place on ESPN2006-06-16T12:00:00Time to reform copyright2006-06-16T13:00:00copyrightfixturesarticle3lawfixturesarticle3Django ReinhardtStephane GrappelliPrinceAchieving self-awareness of Python programs""", format='xml', natural_keys=True) class FixtureTransactionTests(TransactionTestCase): def _dumpdata_assert(self, args, output, format='json'): @@ -329,7 +329,7 @@ class FixtureTransactionTests(TransactionTestCase): ]) # Dump the current contents of the database as a JSON fixture - self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') + self._dumpdata_assert(['fixtures'], '[{"pk": 1, "model": "fixtures.category", "fields": {"description": "Latest news stories", "title": "News Stories"}}, {"pk": 2, "model": "fixtures.article", "fields": {"headline": "Poker has no place on ESPN", "pub_date": "2006-06-16T12:00:00"}}, {"pk": 3, "model": "fixtures.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16T13:00:00"}}, {"pk": 10, "model": "fixtures.book", "fields": {"name": "Achieving self-awareness of Python programs", "authors": []}}]') # Load fixture 4 (compressed), using format discovery management.call_command('loaddata', 'fixture4', verbosity=0, commit=False) diff --git a/tests/modeltests/known_related_objects/__init__.py b/tests/modeltests/known_related_objects/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modeltests/known_related_objects/fixtures/tournament.json b/tests/modeltests/known_related_objects/fixtures/tournament.json new file mode 100644 index 0000000000..2f2b1c5627 --- /dev/null +++ b/tests/modeltests/known_related_objects/fixtures/tournament.json @@ -0,0 +1,65 @@ +[ + { + "pk": 1, + "model": "known_related_objects.tournament", + "fields": { + "name": "Tourney 1" + } + }, + { + "pk": 2, + "model": "known_related_objects.tournament", + "fields": { + "name": "Tourney 2" + } + }, + { + "pk": 1, + "model": "known_related_objects.pool", + "fields": { + "tournament": 1, + "name": "T1 Pool 1" + } + }, + { + "pk": 2, + "model": "known_related_objects.pool", + "fields": { + "tournament": 1, + "name": "T1 Pool 2" + } + }, + { + "pk": 3, + "model": "known_related_objects.pool", + "fields": { + "tournament": 2, + "name": "T2 Pool 1" + } + }, + { + "pk": 4, + "model": "known_related_objects.pool", + "fields": { + "tournament": 2, + "name": "T2 Pool 2" + } + }, + { + "pk": 1, + "model": "known_related_objects.poolstyle", + "fields": { + "name": "T1 Pool 2 Style", + "pool": 2 + } + }, + { + "pk": 2, + "model": "known_related_objects.poolstyle", + "fields": { + "name": "T2 Pool 1 Style", + "pool": 3 + } + } +] + diff --git a/tests/modeltests/known_related_objects/models.py b/tests/modeltests/known_related_objects/models.py new file mode 100644 index 0000000000..4c516dd7e8 --- /dev/null +++ b/tests/modeltests/known_related_objects/models.py @@ -0,0 +1,19 @@ +""" +Existing related object instance caching. + +Test that queries are not redone when going back through known relations. +""" + +from django.db import models + +class Tournament(models.Model): + name = models.CharField(max_length=30) + +class Pool(models.Model): + name = models.CharField(max_length=30) + tournament = models.ForeignKey(Tournament) + +class PoolStyle(models.Model): + name = models.CharField(max_length=30) + pool = models.OneToOneField(Pool) + diff --git a/tests/modeltests/known_related_objects/tests.py b/tests/modeltests/known_related_objects/tests.py new file mode 100644 index 0000000000..24feab2241 --- /dev/null +++ b/tests/modeltests/known_related_objects/tests.py @@ -0,0 +1,88 @@ +from __future__ import absolute_import + +from django.test import TestCase + +from .models import Tournament, Pool, PoolStyle + +class ExistingRelatedInstancesTests(TestCase): + fixtures = ['tournament.json'] + + def test_foreign_key(self): + with self.assertNumQueries(2): + tournament = Tournament.objects.get(pk=1) + pool = tournament.pool_set.all()[0] + self.assertIs(tournament, pool.tournament) + + def test_foreign_key_prefetch_related(self): + with self.assertNumQueries(2): + tournament = (Tournament.objects.prefetch_related('pool_set').get(pk=1)) + pool = tournament.pool_set.all()[0] + self.assertIs(tournament, pool.tournament) + + def test_foreign_key_multiple_prefetch(self): + with self.assertNumQueries(2): + tournaments = list(Tournament.objects.prefetch_related('pool_set')) + pool1 = tournaments[0].pool_set.all()[0] + self.assertIs(tournaments[0], pool1.tournament) + pool2 = tournaments[1].pool_set.all()[0] + self.assertIs(tournaments[1], pool2.tournament) + + def test_one_to_one(self): + with self.assertNumQueries(2): + style = PoolStyle.objects.get(pk=1) + pool = style.pool + self.assertIs(style, pool.poolstyle) + + def test_one_to_one_select_related(self): + with self.assertNumQueries(1): + style = PoolStyle.objects.select_related('pool').get(pk=1) + pool = style.pool + self.assertIs(style, pool.poolstyle) + + def test_one_to_one_multi_select_related(self): + with self.assertNumQueries(1): + poolstyles = list(PoolStyle.objects.select_related('pool')) + self.assertIs(poolstyles[0], poolstyles[0].pool.poolstyle) + self.assertIs(poolstyles[1], poolstyles[1].pool.poolstyle) + + def test_one_to_one_prefetch_related(self): + with self.assertNumQueries(2): + style = PoolStyle.objects.prefetch_related('pool').get(pk=1) + pool = style.pool + self.assertIs(style, pool.poolstyle) + + def test_one_to_one_multi_prefetch_related(self): + with self.assertNumQueries(2): + poolstyles = list(PoolStyle.objects.prefetch_related('pool')) + self.assertIs(poolstyles[0], poolstyles[0].pool.poolstyle) + self.assertIs(poolstyles[1], poolstyles[1].pool.poolstyle) + + def test_reverse_one_to_one(self): + with self.assertNumQueries(2): + pool = Pool.objects.get(pk=2) + style = pool.poolstyle + self.assertIs(pool, style.pool) + + def test_reverse_one_to_one_select_related(self): + with self.assertNumQueries(1): + pool = Pool.objects.select_related('poolstyle').get(pk=2) + style = pool.poolstyle + self.assertIs(pool, style.pool) + + def test_reverse_one_to_one_prefetch_related(self): + with self.assertNumQueries(2): + pool = Pool.objects.prefetch_related('poolstyle').get(pk=2) + style = pool.poolstyle + self.assertIs(pool, style.pool) + + def test_reverse_one_to_one_multi_select_related(self): + with self.assertNumQueries(1): + pools = list(Pool.objects.select_related('poolstyle')) + self.assertIs(pools[1], pools[1].poolstyle.pool) + self.assertIs(pools[2], pools[2].poolstyle.pool) + + def test_reverse_one_to_one_multi_prefetch_related(self): + with self.assertNumQueries(2): + pools = list(Pool.objects.prefetch_related('poolstyle')) + self.assertIs(pools[1], pools[1].poolstyle.pool) + self.assertIs(pools[2], pools[2].poolstyle.pool) diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index fdaae5b852..af8bcbca4e 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -1126,7 +1126,7 @@ class OldFormForXTests(TestCase): f = TextFileForm( data={'description': u'Assistance'}, - files={'file': SimpleUploadedFile('test1.txt', 'hello world')}) + files={'file': SimpleUploadedFile('test1.txt', b'hello world')}) self.assertEqual(f.is_valid(), True) self.assertEqual(type(f.cleaned_data['file']), SimpleUploadedFile) instance = f.save() @@ -1135,7 +1135,7 @@ class OldFormForXTests(TestCase): instance.file.delete() f = TextFileForm( data={'description': u'Assistance'}, - files={'file': SimpleUploadedFile('test1.txt', 'hello world')}) + files={'file': SimpleUploadedFile('test1.txt', b'hello world')}) self.assertEqual(f.is_valid(), True) self.assertEqual(type(f.cleaned_data['file']), SimpleUploadedFile) instance = f.save() @@ -1144,7 +1144,7 @@ class OldFormForXTests(TestCase): # Check if the max_length attribute has been inherited from the model. f = TextFileForm( data={'description': u'Assistance'}, - files={'file': SimpleUploadedFile('test-maxlength.txt', 'hello world')}) + files={'file': SimpleUploadedFile('test-maxlength.txt', b'hello world')}) self.assertEqual(f.is_valid(), False) # Edit an instance that already has the file defined in the model. This will not @@ -1165,7 +1165,7 @@ class OldFormForXTests(TestCase): f = TextFileForm( data={'description': u'Assistance'}, - files={'file': SimpleUploadedFile('test2.txt', 'hello world')}, instance=instance) + files={'file': SimpleUploadedFile('test2.txt', b'hello world')}, instance=instance) self.assertEqual(f.is_valid(), True) instance = f.save() self.assertEqual(instance.file.name, 'tests/test2.txt') @@ -1174,7 +1174,7 @@ class OldFormForXTests(TestCase): instance.file.delete() f = TextFileForm( data={'description': u'Assistance'}, - files={'file': SimpleUploadedFile('test2.txt', 'hello world')}) + files={'file': SimpleUploadedFile('test2.txt', b'hello world')}) self.assertEqual(f.is_valid(), True) instance = f.save() self.assertEqual(instance.file.name, 'tests/test2.txt') @@ -1193,7 +1193,7 @@ class OldFormForXTests(TestCase): f = TextFileForm( data={'description': u'Assistance'}, - files={'file': SimpleUploadedFile('test3.txt', 'hello world')}, instance=instance) + files={'file': SimpleUploadedFile('test3.txt', b'hello world')}, instance=instance) self.assertEqual(f.is_valid(), True) instance = f.save() self.assertEqual(instance.file.name, 'tests/test3.txt') @@ -1215,7 +1215,7 @@ class OldFormForXTests(TestCase): f = TextFileForm( data={'description': u'Assistance'}, - files={'file': SimpleUploadedFile('test3.txt', 'hello world')}) + files={'file': SimpleUploadedFile('test3.txt', b'hello world')}) self.assertEqual(f.is_valid(), True) instance = f.save() self.assertEqual(instance.file.name, 'tests/test3.txt') diff --git a/tests/modeltests/model_inheritance/tests.py b/tests/modeltests/model_inheritance/tests.py index 2e1a7a5a9c..695d3f836c 100644 --- a/tests/modeltests/model_inheritance/tests.py +++ b/tests/modeltests/model_inheritance/tests.py @@ -275,3 +275,21 @@ class ModelInheritanceTests(TestCase): def test_mixin_init(self): m = MixinModel() self.assertEqual(m.other_attr, 1) + + def test_update_query_counts(self): + """ + Test that update queries do not generate non-necessary queries. + Refs #18304. + """ + c = Chef.objects.create(name="Albert") + ir = ItalianRestaurant.objects.create( + name="Ristorante Miron", + address="1234 W. Ash", + serves_hot_dogs=False, + serves_pizza=False, + serves_gnocchi=True, + rating=4, + chef=c + ) + with self.assertNumQueries(6): + ir.save() diff --git a/tests/modeltests/serializers/tests.py b/tests/modeltests/serializers/tests.py index dac098763c..e792c944a1 100644 --- a/tests/modeltests/serializers/tests.py +++ b/tests/modeltests/serializers/tests.py @@ -113,8 +113,8 @@ class SerializersTestBase(object): Tests the ability to create new objects by modifying serialized content. """ - old_headline = "Poker has no place on ESPN" - new_headline = "Poker has no place on television" + old_headline = b"Poker has no place on ESPN" + new_headline = b"Poker has no place on television" serial_str = serializers.serialize(self.serializer_name, Article.objects.all()) serial_str = serial_str.replace(old_headline, new_headline) @@ -284,7 +284,7 @@ class SerializersTransactionTestBase(object): class XmlSerializerTestCase(SerializersTestBase, TestCase): serializer_name = "xml" - pkless_str = """ + pkless_str = b""" Reference @@ -330,7 +330,7 @@ class XmlSerializerTestCase(SerializersTestBase, TestCase): class XmlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): serializer_name = "xml" - fwd_ref_str = """ + fwd_ref_str = b""" 1 @@ -350,7 +350,7 @@ class XmlSerializerTransactionTestCase(SerializersTransactionTestBase, Transacti class JsonSerializerTestCase(SerializersTestBase, TestCase): serializer_name = "json" - pkless_str = """[{"pk": null, "model": "serializers.category", "fields": {"name": "Reference"}}]""" + pkless_str = b"""[{"pk": null, "model": "serializers.category", "fields": {"name": "Reference"}}]""" @staticmethod def _validate_output(serial_str): @@ -380,7 +380,7 @@ class JsonSerializerTestCase(SerializersTestBase, TestCase): class JsonSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): serializer_name = "json" - fwd_ref_str = """[ + fwd_ref_str = b"""[ { "pk": 1, "model": "serializers.article", @@ -413,7 +413,7 @@ except ImportError: else: class YamlSerializerTestCase(SerializersTestBase, TestCase): serializer_name = "yaml" - fwd_ref_str = """- fields: + fwd_ref_str = b"""- fields: headline: Forward references pose no problem pub_date: 2006-06-16 15:00:00 categories: [1] @@ -429,7 +429,7 @@ else: pk: 1 model: serializers.author""" - pkless_str = """- fields: + pkless_str = b"""- fields: name: Reference pk: null model: serializers.category""" @@ -469,7 +469,7 @@ else: class YamlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): serializer_name = "yaml" - fwd_ref_str = """- fields: + fwd_ref_str = b"""- fields: headline: Forward references pose no problem pub_date: 2006-06-16 15:00:00 categories: [1] diff --git a/tests/modeltests/str/tests.py b/tests/modeltests/str/tests.py index 9f6cf7ad96..1b0eb0972f 100644 --- a/tests/modeltests/str/tests.py +++ b/tests/modeltests/str/tests.py @@ -11,11 +11,11 @@ from .models import Article, InternationalArticle class SimpleTests(TestCase): def test_basic(self): a = Article.objects.create( - headline='Area man programs in Python', + headline=b'Area man programs in Python', pub_date=datetime.datetime(2005, 7, 28) ) - self.assertEqual(str(a), 'Area man programs in Python') - self.assertEqual(repr(a), '') + self.assertEqual(str(a), b'Area man programs in Python') + self.assertEqual(repr(a), b'') def test_international(self): a = InternationalArticle.objects.create( @@ -23,4 +23,4 @@ class SimpleTests(TestCase): pub_date=datetime.datetime(2005, 7, 28) ) # The default str() output will be the UTF-8 encoded output of __unicode__(). - self.assertEqual(str(a), 'Girl wins \xe2\x82\xac12.500 in lottery') \ No newline at end of file + self.assertEqual(str(a), b'Girl wins \xe2\x82\xac12.500 in lottery') diff --git a/tests/modeltests/update_only_fields/tests.py b/tests/modeltests/update_only_fields/tests.py index 8609821a44..e843bd7ab9 100644 --- a/tests/modeltests/update_only_fields/tests.py +++ b/tests/modeltests/update_only_fields/tests.py @@ -1,6 +1,6 @@ from __future__ import absolute_import -from django.test import TestCase, skipUnlessDBFeature +from django.test import TestCase from django.db.models.signals import pre_save, post_save from .models import Person, Employee, ProxyEmployee, Profile, Account @@ -123,9 +123,6 @@ class UpdateOnlyFieldsTests(TestCase): self.assertEqual(len(pre_save_data), 0) self.assertEqual(len(post_save_data), 0) - # A bug in SQLUpdateCompiler prevents this test from succeeding on MySQL - # Require update_can_self_select for this test for now. Refs #18304. - @skipUnlessDBFeature('update_can_self_select') def test_num_queries_inheritance(self): s = Employee.objects.create(name='Sara', gender='F') s.employee_num = 1 diff --git a/tests/modeltests/user_commands/management/commands/dance.py b/tests/modeltests/user_commands/management/commands/dance.py index 4ad5579e1e..911530d223 100644 --- a/tests/modeltests/user_commands/management/commands/dance.py +++ b/tests/modeltests/user_commands/management/commands/dance.py @@ -1,6 +1,6 @@ from optparse import make_option -from django.core.management.base import BaseCommand +from django.core.management.base import BaseCommand, CommandError class Command(BaseCommand): @@ -8,11 +8,13 @@ class Command(BaseCommand): args = '' requires_model_validation = True - option_list =[ + option_list = BaseCommand.option_list + ( make_option("-s", "--style", default="Rock'n'Roll"), make_option("-x", "--example") - ] + ) def handle(self, *args, **options): example = options["example"] + if example == "raise": + raise CommandError() self.stdout.write("I don't feel like dancing %s." % options["style"]) diff --git a/tests/modeltests/user_commands/tests.py b/tests/modeltests/user_commands/tests.py index 896dd667ff..509f13f62f 100644 --- a/tests/modeltests/user_commands/tests.py +++ b/tests/modeltests/user_commands/tests.py @@ -1,3 +1,4 @@ +import sys from StringIO import StringIO from django.core import management @@ -11,13 +12,13 @@ class CommandTests(TestCase): out = StringIO() management.call_command('dance', stdout=out) self.assertEqual(out.getvalue(), - "I don't feel like dancing Rock'n'Roll.") + "I don't feel like dancing Rock'n'Roll.\n") def test_command_style(self): out = StringIO() management.call_command('dance', style='Jive', stdout=out) self.assertEqual(out.getvalue(), - "I don't feel like dancing Jive.") + "I don't feel like dancing Jive.\n") def test_language_preserved(self): out = StringIO() @@ -26,4 +27,20 @@ class CommandTests(TestCase): self.assertEqual(translation.get_language(), 'fr') def test_explode(self): + """ Test that an unknown command raises CommandError """ self.assertRaises(CommandError, management.call_command, ('explode',)) + + def test_system_exit(self): + """ Exception raised in a command should raise CommandError with + call_command, but SystemExit when run from command line + """ + with self.assertRaises(CommandError): + management.call_command('dance', example="raise") + old_stderr = sys.stderr + sys.stderr = err = StringIO() + try: + with self.assertRaises(SystemExit): + management.ManagementUtility(['manage.py', 'dance', '--example=raise']).execute() + finally: + sys.stderr = old_stderr + self.assertIn("CommandError", err.getvalue()) diff --git a/tests/modeltests/validation/test_error_messages.py b/tests/modeltests/validation/test_error_messages.py index 04ad7aadf7..4aea3d52de 100644 --- a/tests/modeltests/validation/test_error_messages.py +++ b/tests/modeltests/validation/test_error_messages.py @@ -1,3 +1,4 @@ +# -*- encoding: utf-8 -*- from django.core.exceptions import ValidationError from django.db import models from django.utils.unittest import TestCase @@ -5,142 +6,89 @@ from django.utils.unittest import TestCase class ValidationMessagesTest(TestCase): + def _test_validation_messages(self, field, value, expected): + with self.assertRaises(ValidationError) as cm: + field.clean(value, None) + self.assertEqual(cm.exception.messages, expected) + def test_autofield_field_raises_error_message(self): f = models.AutoField(primary_key=True) - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, [u"'foo' value must be an integer."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value must be an integer."]) # primary_key must be True. Refs #12467. - self.assertRaises(AssertionError, models.AutoField, 'primary_key', False) - try: + with self.assertRaisesRegexp(AssertionError, + "AutoFields must have primary_key=True."): models.AutoField(primary_key=False) - except AssertionError as e: - self.assertEqual(str(e), "AutoFields must have primary_key=True.") def test_integer_field_raises_error_message(self): f = models.IntegerField() - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, [u"'foo' value must be an integer."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value must be an integer."]) def test_boolean_field_raises_error_message(self): f = models.BooleanField() - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, - [u"'foo' value must be either True or False."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value must be either True or False."]) def test_float_field_raises_error_message(self): f = models.FloatField() - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, [u"'foo' value must be a float."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value must be a float."]) def test_decimal_field_raises_error_message(self): f = models.DecimalField() - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, - [u"'foo' value must be a decimal number."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value must be a decimal number."]) def test_null_boolean_field_raises_error_message(self): f = models.NullBooleanField() - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, - [u"'foo' value must be either None, True or False."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value must be either None, True or False."]) def test_date_field_raises_error_message(self): f = models.DateField() - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'foo' value has an invalid date format. " - u"It must be in YYYY-MM-DD format."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value has an invalid date format. " + u"It must be in YYYY-MM-DD format."]) - self.assertRaises(ValidationError, f.clean, 'aaaa-10-10', None) - try: - f.clean('aaaa-10-10', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'aaaa-10-10' value has an invalid date format. " - u"It must be in YYYY-MM-DD format."]) + self._test_validation_messages(f, 'aaaa-10-10', + [u"'aaaa-10-10' value has an invalid date format. " + u"It must be in YYYY-MM-DD format."]) - self.assertRaises(ValidationError, f.clean, '2011-13-10', None) - try: - f.clean('2011-13-10', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'2011-13-10' value has the correct format (YYYY-MM-DD) " - u"but it is an invalid date."]) + self._test_validation_messages(f, '2011-13-10', + [u"'2011-13-10' value has the correct format (YYYY-MM-DD) " + u"but it is an invalid date."]) - self.assertRaises(ValidationError, f.clean, '2011-10-32', None) - try: - f.clean('2011-10-32', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'2011-10-32' value has the correct format (YYYY-MM-DD) " - u"but it is an invalid date."]) + self._test_validation_messages(f, '2011-10-32', + [u"'2011-10-32' value has the correct format (YYYY-MM-DD) " + u"but it is an invalid date."]) def test_datetime_field_raises_error_message(self): f = models.DateTimeField() # Wrong format - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'foo' value has an invalid format. It must be " - u"in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value has an invalid format. It must be " + u"in YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."]) # Correct format but invalid date - self.assertRaises(ValidationError, f.clean, '2011-10-32', None) - try: - f.clean('2011-10-32', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'2011-10-32' value has the correct format " - u"(YYYY-MM-DD) but it is an invalid date."]) + self._test_validation_messages(f, '2011-10-32', + [u"'2011-10-32' value has the correct format " + u"(YYYY-MM-DD) but it is an invalid date."]) # Correct format but invalid date/time - self.assertRaises(ValidationError, f.clean, '2011-10-32 10:10', None) - try: - f.clean('2011-10-32 10:10', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'2011-10-32 10:10' value has the correct format " - u"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) " - u"but it is an invalid date/time."]) + self._test_validation_messages(f, '2011-10-32 10:10', + [u"'2011-10-32 10:10' value has the correct format " + u"(YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) " + u"but it is an invalid date/time."]) def test_time_field_raises_error_message(self): f = models.TimeField() # Wrong format - self.assertRaises(ValidationError, f.clean, 'foo', None) - try: - f.clean('foo', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'foo' value has an invalid format. It must be in " - u"HH:MM[:ss[.uuuuuu]] format."]) + self._test_validation_messages(f, u'fõo', + [u"'fõo' value has an invalid format. It must be in " + u"HH:MM[:ss[.uuuuuu]] format."]) + # Correct format but invalid time - self.assertRaises(ValidationError, f.clean, '25:50', None) - try: - f.clean('25:50', None) - except ValidationError as e: - self.assertEqual(e.messages, [ - u"'25:50' value has the correct format " - u"(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."]) + self._test_validation_messages(f, '25:50', + [u"'25:50' value has the correct format " + u"(HH:MM[:ss[.uuuuuu]]) but it is an invalid time."]) diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 98492ffbdd..ecb16df53a 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -13,13 +13,12 @@ import sys from django import conf, bin, get_version from django.conf import settings +from django.db import connection from django.test.simple import DjangoTestSuiteRunner from django.utils import unittest from django.test import LiveServerTestCase test_dir = os.path.dirname(os.path.dirname(__file__)) -expected_query_re = re.compile(r'CREATE TABLE [`"]admin_scripts_article[`"]', re.IGNORECASE) - class AdminScriptTestCase(unittest.TestCase): def write_settings(self, filename, apps=None, is_dir=False, sdict=None): @@ -859,14 +858,18 @@ class ManageAlternateSettings(AdminScriptTestCase): "alternate: manage.py builtin commands work with settings provided as argument" args = ['sqlall', '--settings=alternate_settings', 'admin_scripts'] out, err = self.run_manage(args) - self.assertRegexpMatches(out, expected_query_re) + expected = ('create table %s' + % connection.ops.quote_name('admin_scripts_article')) + self.assertTrue(expected.lower() in out.lower()) self.assertNoOutput(err) def test_builtin_with_environment(self): "alternate: manage.py builtin commands work if settings are provided in the environment" args = ['sqlall', 'admin_scripts'] out, err = self.run_manage(args, 'alternate_settings') - self.assertRegexpMatches(out, expected_query_re) + expected = ('create table %s' + % connection.ops.quote_name('admin_scripts_article')) + self.assertTrue(expected.lower() in out.lower()) self.assertNoOutput(err) def test_builtin_with_bad_settings(self): diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index 8113f2e16d..055f494dcd 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -170,7 +170,7 @@ class UtilTests(unittest.TestCase): ) self.assertEqual( label_for_field("__str__", Article), - "article" + b"article" ) self.assertRaises( diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py index 8de68b2404..be1d294051 100644 --- a/tests/regressiontests/admin_widgets/tests.py +++ b/tests/regressiontests/admin_widgets/tests.py @@ -280,7 +280,7 @@ class AdminFileWidgetTest(DjangoTestCase): ) self.assertHTMLEqual( - conditional_escape(w.render('test', SimpleUploadedFile('test', 'content'))), + conditional_escape(w.render('test', SimpleUploadedFile('test', b'content'))), '', ) diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index eec817c5a2..109e1a5bcc 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -89,6 +89,12 @@ class MySQLTests(TestCase): else: self.assertFalse(found_reset) + @unittest.skipUnless(connection.vendor == 'mysql', + "Test valid only for MySQL") + def test_server_version_connections(self): + connection.close() + connection.mysql_version + self.assertTrue(connection.connection is None) class DateQuotingTest(TestCase): @@ -397,6 +403,12 @@ class BackendTestCase(TestCase): self.assertTrue(hasattr(connection.ops, 'connection')) self.assertEqual(connection, connection.ops.connection) + def test_supports_needed_confirm(self): + connection.features.confirm() + self.assertIn(connection.features.supports_transactions, (True, False)) + self.assertIn(connection.features.supports_stddev, (True, False)) + self.assertIn(connection.features.can_introspect_foreign_keys, (True, False)) + def test_duplicate_table_error(self): """ Test that creating an existing table returns a DatabaseError """ cursor = connection.cursor() diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index 780718b487..88fe5471cf 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -7,7 +7,6 @@ from __future__ import absolute_import import hashlib import os import re -import StringIO import tempfile import time import warnings @@ -820,9 +819,14 @@ class DBCacheTests(BaseCacheTests, TransactionTestCase): self.perform_cull_test(50, 18) def test_second_call_doesnt_crash(self): - err = StringIO.StringIO() - management.call_command('createcachetable', self._table_name, verbosity=0, interactive=False, stderr=err) - self.assertTrue("Cache table 'test cache table' could not be created" in err.getvalue()) + with self.assertRaisesRegexp(management.CommandError, + "Cache table 'test cache table' could not be created"): + management.call_command( + 'createcachetable', + self._table_name, + verbosity=0, + interactive=False + ) @override_settings(USE_TZ=True) diff --git a/tests/regressiontests/conditional_processing/models.py b/tests/regressiontests/conditional_processing/models.py index f7f48bc9c8..97aeff5eaa 100644 --- a/tests/regressiontests/conditional_processing/models.py +++ b/tests/regressiontests/conditional_processing/models.py @@ -63,10 +63,10 @@ class ConditionalGet(TestCase): def testIfMatch(self): self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % ETAG - response = self.client.put('/condition/etag/', {'data': ''}) + response = self.client.put('/condition/etag/') self.assertEqual(response.status_code, 200) self.client.defaults['HTTP_IF_MATCH'] = '"%s"' % EXPIRED_ETAG - response = self.client.put('/condition/etag/', {'data': ''}) + response = self.client.put('/condition/etag/') self.assertEqual(response.status_code, 412) def testBothHeaders(self): diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py index d403951ae2..2d9b4f755f 100644 --- a/tests/regressiontests/csrf_tests/tests.py +++ b/tests/regressiontests/csrf_tests/tests.py @@ -47,7 +47,7 @@ class TestingHttpRequest(HttpRequest): class CsrfViewMiddlewareTest(TestCase): # The csrf token is potentially from an untrusted source, so could have # characters that need dealing with. - _csrf_id_cookie = "<1>\xc2\xa1" + _csrf_id_cookie = b"<1>\xc2\xa1" _csrf_id = "1" def _get_GET_no_csrf_cookie_request(self): diff --git a/tests/regressiontests/file_storage/tests.py b/tests/regressiontests/file_storage/tests.py index ce64c5de23..87035d96d7 100644 --- a/tests/regressiontests/file_storage/tests.py +++ b/tests/regressiontests/file_storage/tests.py @@ -127,7 +127,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile('custom contents') + f = ContentFile(b'custom contents') f_name = self.storage.save('test.file', f) atime = self.storage.accessed_time(f_name) @@ -143,7 +143,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile('custom contents') + f = ContentFile(b'custom contents') f_name = self.storage.save('test.file', f) ctime = self.storage.created_time(f_name) @@ -160,7 +160,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile('custom contents') + f = ContentFile(b'custom contents') f_name = self.storage.save('test.file', f) mtime = self.storage.modified_time(f_name) @@ -177,7 +177,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile('custom contents') + f = ContentFile(b'custom contents') f.name = 'test.file' storage_f_name = self.storage.save(None, f) @@ -194,11 +194,11 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('path/to')) self.storage.save('path/to/test.file', - ContentFile('file saved with path')) + ContentFile(b'file saved with path')) self.assertTrue(self.storage.exists('path/to')) self.assertEqual(self.storage.open('path/to/test.file').read(), - 'file saved with path') + b'file saved with path') self.assertTrue(os.path.exists( os.path.join(self.temp_dir, 'path', 'to', 'test.file'))) @@ -211,7 +211,7 @@ class FileStorageTests(unittest.TestCase): """ self.assertFalse(self.storage.exists('test.file')) - f = ContentFile('custom contents') + f = ContentFile(b'custom contents') f_name = self.storage.save('test.file', f) self.assertEqual(self.storage.path(f_name), @@ -246,8 +246,8 @@ class FileStorageTests(unittest.TestCase): self.assertFalse(self.storage.exists('storage_test_2')) self.assertFalse(self.storage.exists('storage_dir_1')) - f = self.storage.save('storage_test_1', ContentFile('custom content')) - f = self.storage.save('storage_test_2', ContentFile('custom content')) + f = self.storage.save('storage_test_1', ContentFile(b'custom content')) + f = self.storage.save('storage_test_2', ContentFile(b'custom content')) os.mkdir(os.path.join(self.temp_dir, 'storage_dir_1')) dirs, files = self.storage.listdir('') @@ -304,18 +304,18 @@ class FileStorageTests(unittest.TestCase): os.makedirs = fake_makedirs self.storage.save('normal/test.file', - ContentFile('saved normally')) + ContentFile(b'saved normally')) self.assertEqual(self.storage.open('normal/test.file').read(), - 'saved normally') + b'saved normally') self.storage.save('raced/test.file', - ContentFile('saved with race')) + ContentFile(b'saved with race')) self.assertEqual(self.storage.open('raced/test.file').read(), - 'saved with race') + b'saved with race') # Check that OSErrors aside from EEXIST are still raised. self.assertRaises(OSError, - self.storage.save, 'error/test.file', ContentFile('not saved')) + self.storage.save, 'error/test.file', ContentFile(b'not saved')) finally: os.makedirs = real_makedirs @@ -341,16 +341,16 @@ class FileStorageTests(unittest.TestCase): try: os.remove = fake_remove - self.storage.save('normal.file', ContentFile('delete normally')) + self.storage.save('normal.file', ContentFile(b'delete normally')) self.storage.delete('normal.file') self.assertFalse(self.storage.exists('normal.file')) - self.storage.save('raced.file', ContentFile('delete with race')) + self.storage.save('raced.file', ContentFile(b'delete with race')) self.storage.delete('raced.file') self.assertFalse(self.storage.exists('normal.file')) # Check that OSErrors aside from ENOENT are still raised. - self.storage.save('error.file', ContentFile('delete with error')) + self.storage.save('error.file', ContentFile(b'delete with error')) self.assertRaises(OSError, self.storage.delete, 'error.file') finally: os.remove = real_remove @@ -374,9 +374,9 @@ class CustomStorageTests(FileStorageTests): storage_class = CustomStorage def test_custom_get_available_name(self): - first = self.storage.save('custom_storage', ContentFile('custom contents')) + first = self.storage.save('custom_storage', ContentFile(b'custom contents')) self.assertEqual(first, 'custom_storage') - second = self.storage.save('custom_storage', ContentFile('more contents')) + second = self.storage.save('custom_storage', ContentFile(b'more contents')) self.assertEqual(second, 'custom_storage.2') self.storage.delete(first) self.storage.delete(second) @@ -410,7 +410,7 @@ class FileSaveRaceConditionTest(unittest.TestCase): shutil.rmtree(self.storage_dir) def save_file(self, name): - name = self.storage.save(name, SlowFile("Data")) + name = self.storage.save(name, SlowFile(b"Data")) def test_race_condition(self): self.thread.start() @@ -433,7 +433,7 @@ class FileStoragePermissions(unittest.TestCase): shutil.rmtree(self.storage_dir) def test_file_upload_permissions(self): - name = self.storage.save("the_file", ContentFile("data")) + name = self.storage.save("the_file", ContentFile(b"data")) actual_mode = os.stat(self.storage.path(name))[0] & 0777 self.assertEqual(actual_mode, 0666) @@ -453,8 +453,8 @@ class FileStoragePathParsing(unittest.TestCase): sure we still mangle the file name instead of the directory name. """ - self.storage.save('dotted.path/test', ContentFile("1")) - self.storage.save('dotted.path/test', ContentFile("2")) + self.storage.save('dotted.path/test', ContentFile(b"1")) + self.storage.save('dotted.path/test', ContentFile(b"2")) self.assertFalse(os.path.exists(os.path.join(self.storage_dir, 'dotted_.path'))) self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/test'))) @@ -465,8 +465,8 @@ class FileStoragePathParsing(unittest.TestCase): File names with a dot as their first character don't have an extension, and the underscore should get added to the end. """ - self.storage.save('dotted.path/.test', ContentFile("1")) - self.storage.save('dotted.path/.test', ContentFile("2")) + self.storage.save('dotted.path/.test', ContentFile(b"1")) + self.storage.save('dotted.path/.test', ContentFile(b"2")) self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test'))) self.assertTrue(os.path.exists(os.path.join(self.storage_dir, 'dotted.path/.test_1'))) @@ -542,11 +542,11 @@ class ContentFileTestCase(unittest.TestCase): Test that the constructor of ContentFile accepts 'name' (#16590). """ def test_content_file_default_name(self): - self.assertEqual(ContentFile("content").name, None) + self.assertEqual(ContentFile(b"content").name, None) def test_content_file_custom_name(self): name = "I can have a name too!" - self.assertEqual(ContentFile("content", name=name).name, name) + self.assertEqual(ContentFile(b"content", name=name).name, name) class NoNameFileTestCase(unittest.TestCase): """ diff --git a/tests/regressiontests/file_uploads/tests.py b/tests/regressiontests/file_uploads/tests.py index de552d88ec..c8de8035f9 100644 --- a/tests/regressiontests/file_uploads/tests.py +++ b/tests/regressiontests/file_uploads/tests.py @@ -24,7 +24,7 @@ UNICODE_FILENAME = u'test-0123456789_中文_Orléans.jpg' class FileUploadTests(TestCase): def test_simple_upload(self): - with open(__file__) as fp: + with open(__file__, 'rb') as fp: post_data = { 'name': 'Ringo', 'file_field': fp, @@ -36,11 +36,11 @@ class FileUploadTests(TestCase): tdir = tempfile.gettempdir() file1 = tempfile.NamedTemporaryFile(suffix=".file1", dir=tdir) - file1.write('a' * (2 ** 21)) + file1.write(b'a' * (2 ** 21)) file1.seek(0) file2 = tempfile.NamedTemporaryFile(suffix=".file2", dir=tdir) - file2.write('a' * (10 * 2 ** 20)) + file2.write(b'a' * (10 * 2 ** 20)) file2.seek(0) post_data = { @@ -89,7 +89,7 @@ class FileUploadTests(TestCase): # This file contains chinese symbols and an accented char in the name. with open(os.path.join(tdir, UNICODE_FILENAME.encode('utf-8')), 'w+b') as file1: - file1.write('b' * (2 ** 10)) + file1.write(b'b' * (2 ** 10)) file1.seek(0) post_data = { @@ -214,7 +214,7 @@ class FileUploadTests(TestCase): 'CONTENT_TYPE': client.MULTIPART_CONTENT, 'PATH_INFO': '/file_uploads/echo/', 'REQUEST_METHOD': 'POST', - 'wsgi.input': client.FakePayload(''), + 'wsgi.input': client.FakePayload(b''), } got = json.loads(self.client.request(**r).content) self.assertEqual(got, {}) @@ -222,12 +222,12 @@ class FileUploadTests(TestCase): def test_custom_upload_handler(self): # A small file (under the 5M quota) smallfile = tempfile.NamedTemporaryFile() - smallfile.write('a' * (2 ** 21)) + smallfile.write(b'a' * (2 ** 21)) smallfile.seek(0) # A big file (over the quota) bigfile = tempfile.NamedTemporaryFile() - bigfile.write('a' * (10 * 2 ** 20)) + bigfile.write(b'a' * (10 * 2 ** 20)) bigfile.seek(0) # Small file posting should work. @@ -242,7 +242,7 @@ class FileUploadTests(TestCase): def test_broken_custom_upload_handler(self): f = tempfile.NamedTemporaryFile() - f.write('a' * (2 ** 21)) + f.write(b'a' * (2 ** 21)) f.seek(0) # AttributeError: You cannot alter upload handlers after the upload has been processed. @@ -255,15 +255,15 @@ class FileUploadTests(TestCase): def test_fileupload_getlist(self): file1 = tempfile.NamedTemporaryFile() - file1.write('a' * (2 ** 23)) + file1.write(b'a' * (2 ** 23)) file1.seek(0) file2 = tempfile.NamedTemporaryFile() - file2.write('a' * (2 * 2 ** 18)) + file2.write(b'a' * (2 * 2 ** 18)) file2.seek(0) file2a = tempfile.NamedTemporaryFile() - file2a.write('a' * (5 * 2 ** 20)) + file2a.write(b'a' * (5 * 2 ** 20)) file2a.seek(0) response = self.client.post('/file_uploads/getlist_count/', { @@ -299,14 +299,14 @@ class FileUploadTests(TestCase): # this test would fail. So we need to know exactly what kind of error # it raises when there is an attempt to read more than the available bytes: try: - client.FakePayload('a').read(2) + client.FakePayload(b'a').read(2) except Exception as reference_error: pass # install the custom handler that tries to access request.POST self.client.handler = POSTAccessingHandler() - with open(__file__) as fp: + with open(__file__, 'rb') as fp: post_data = { 'name': 'Ringo', 'file_field': fp, @@ -374,7 +374,7 @@ class DirectoryCreationTests(unittest.TestCase): """Permission errors are not swallowed""" os.chmod(temp_storage.location, 0500) try: - self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x')) + self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', b'x')) except OSError as err: self.assertEqual(err.errno, errno.EACCES) except Exception: @@ -383,9 +383,9 @@ class DirectoryCreationTests(unittest.TestCase): def test_not_a_directory(self): """The correct IOError is raised when the upload directory name exists but isn't a directory""" # Create a file with the upload directory name - open(UPLOAD_TO, 'w').close() + open(UPLOAD_TO, 'wb').close() try: - self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', 'x')) + self.obj.testfile.save('foo.txt', SimpleUploadedFile('foo.txt', b'x')) except IOError as err: # The test needs to be done on a specific string as IOError # is raised even without the patch (just not early enough) diff --git a/tests/regressiontests/fixtures_regress/tests.py b/tests/regressiontests/fixtures_regress/tests.py index 5c6f758b66..0e2cc3a9b2 100644 --- a/tests/regressiontests/fixtures_regress/tests.py +++ b/tests/regressiontests/fixtures_regress/tests.py @@ -185,25 +185,6 @@ class TestFixtures(TestCase): "No fixture data found for 'empty'. (File format may be invalid.)\n" ) - def test_abort_loaddata_on_error(self): - """ - Test for ticket #4371 -- If any of the fixtures contain an error, - loading is aborted. - Validate that error conditions are caught correctly - """ - stderr = BytesIO() - management.call_command( - 'loaddata', - 'empty', - verbosity=0, - commit=False, - stderr=stderr, - ) - self.assertEqual( - stderr.getvalue(), - "No fixture data found for 'empty'. (File format may be invalid.)\n" - ) - def test_error_message(self): """ (Regression for #9011 - error message is correct) diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index a7d98ec7d4..4d442de382 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -478,6 +478,14 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "[u'Ensure this value has at most 10 characters (it has 11).']", f.clean, '12345678901') self.assertRaisesMessage(ValidationError, "[u'Enter a valid value.']", f.clean, '12345a') + def test_regexfield_6(self): + """ + Ensure that it works with unicode characters. + Refs #. + """ + f = RegexField('^\w+$') + self.assertEqual(u'éèøçÎÎ你好', f.clean(u'éèøçÎÎ你好')) + def test_change_regex_after_init(self): f = RegexField('^[a-z]+$') f.regex = '^\d+$' @@ -540,27 +548,27 @@ class FieldsTests(SimpleTestCase): self.assertRaisesMessage(ValidationError, "[u'This field is required.']", f.clean, None) self.assertRaisesMessage(ValidationError, "[u'This field is required.']", f.clean, None, '') self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf')) - self.assertRaisesMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', '')) - self.assertRaisesMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', ''), '') + self.assertRaisesMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', b'')) + self.assertRaisesMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, SimpleUploadedFile('', b''), '') self.assertEqual('files/test3.pdf', f.clean(None, 'files/test3.pdf')) self.assertRaisesMessage(ValidationError, "[u'No file was submitted. Check the encoding type on the form.']", f.clean, 'some content that is not a file') self.assertRaisesMessage(ValidationError, "[u'The submitted file is empty.']", f.clean, SimpleUploadedFile('name', None)) - self.assertRaisesMessage(ValidationError, "[u'The submitted file is empty.']", f.clean, SimpleUploadedFile('name', '')) - self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content')))) - self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')))) - self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content'), 'files/test4.pdf'))) + self.assertRaisesMessage(ValidationError, "[u'The submitted file is empty.']", f.clean, SimpleUploadedFile('name', b'')) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', b'Some File Content')))) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', u'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह'.encode('utf-8'))))) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', b'Some File Content'), 'files/test4.pdf'))) def test_filefield_2(self): f = FileField(max_length = 5) - self.assertRaisesMessage(ValidationError, "[u'Ensure this filename has at most 5 characters (it has 18).']", f.clean, SimpleUploadedFile('test_maxlength.txt', 'hello world')) + self.assertRaisesMessage(ValidationError, "[u'Ensure this filename has at most 5 characters (it has 18).']", f.clean, SimpleUploadedFile('test_maxlength.txt', b'hello world')) self.assertEqual('files/test1.pdf', f.clean('', 'files/test1.pdf')) self.assertEqual('files/test2.pdf', f.clean(None, 'files/test2.pdf')) - self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', 'Some File Content')))) + self.assertEqual(SimpleUploadedFile, type(f.clean(SimpleUploadedFile('name', b'Some File Content')))) def test_filefield_3(self): f = FileField(allow_empty_file=True) self.assertEqual(SimpleUploadedFile, - type(f.clean(SimpleUploadedFile('name', '')))) + type(f.clean(SimpleUploadedFile('name', b'')))) # URLField ################################################################## @@ -977,39 +985,30 @@ class FieldsTests(SimpleTestCase): self.assertTrue(got[0].endswith(exp[0])) def test_filepathfield_folders(self): - path = forms.__file__ - path = os.path.dirname(path) + '/' + path = os.path.dirname(__file__) + '/filepath_test_files/' f = FilePathField(path=path, allow_folders=True, allow_files=False) f.choices.sort() expected = [ - ('/django/forms/extras', 'extras'), + ('/tests/regressiontests/forms/tests/filepath_test_files/directory', 'directory'), ] for exp, got in zip(expected, fix_os_paths(f.choices)): self.assertEqual(exp[1], got[1]) - self.assert_(got[0].endswith(exp[0])) + self.assertTrue(got[0].endswith(exp[0])) f = FilePathField(path=path, allow_folders=True, allow_files=True) f.choices.sort() expected = [ - ('/django/forms/__init__.py', '__init__.py'), - ('/django/forms/__init__.pyc', '__init__.pyc'), - ('/django/forms/extras', 'extras'), - ('/django/forms/fields.py', 'fields.py'), - ('/django/forms/fields.pyc', 'fields.pyc'), - ('/django/forms/forms.py', 'forms.py'), - ('/django/forms/forms.pyc', 'forms.pyc'), - ('/django/forms/formsets.py', 'formsets.py'), - ('/django/forms/formsets.pyc', 'formsets.pyc'), - ('/django/forms/models.py', 'models.py'), - ('/django/forms/models.pyc', 'models.pyc'), - ('/django/forms/util.py', 'util.py'), - ('/django/forms/util.pyc', 'util.pyc'), - ('/django/forms/widgets.py', 'widgets.py'), - ('/django/forms/widgets.pyc', 'widgets.pyc') + ('/tests/regressiontests/forms/tests/filepath_test_files/.dot-file', '.dot-file'), + ('/tests/regressiontests/forms/tests/filepath_test_files/directory', 'directory'), + ('/tests/regressiontests/forms/tests/filepath_test_files/fake-image.jpg', 'fake-image.jpg'), + ('/tests/regressiontests/forms/tests/filepath_test_files/real-text-file.txt', 'real-text-file.txt'), ] - for exp, got in zip(expected, fix_os_paths(f.choices)): - self.assertEqual(exp[1], got[1]) + + actual = fix_os_paths(f.choices) + self.assertEqual(len(expected), len(actual)) + for exp, got in zip(expected, actual): self.assertEqual(exp[1], got[1]) + self.assertTrue(got[0].endswith(exp[0])) # SplitDateTimeField ########################################################## diff --git a/tests/regressiontests/forms/tests/filepath_test_files/.dot-file b/tests/regressiontests/forms/tests/filepath_test_files/.dot-file new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/forms/tests/filepath_test_files/directory/.keep b/tests/regressiontests/forms/tests/filepath_test_files/directory/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/forms/tests/filepath_test_files/fake-image.jpg b/tests/regressiontests/forms/tests/filepath_test_files/fake-image.jpg new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/forms/tests/filepath_test_files/real-text-file.txt b/tests/regressiontests/forms/tests/filepath_test_files/real-text-file.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py index 3f529f23db..16cf46fc29 100644 --- a/tests/regressiontests/forms/tests/forms.py +++ b/tests/regressiontests/forms/tests/forms.py @@ -1463,17 +1463,17 @@ class FormsTestCase(TestCase): f = FileForm(data={}, files={}, auto_id=False) self.assertHTMLEqual(f.as_table(), '
    ') - f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', '')}, auto_id=False) + f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', b'')}, auto_id=False) self.assertHTMLEqual(f.as_table(), '') f = FileForm(data={}, files={'file1': 'something that is not a file'}, auto_id=False) self.assertHTMLEqual(f.as_table(), '') - f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', 'some content')}, auto_id=False) + f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', b'some content')}, auto_id=False) self.assertHTMLEqual(f.as_table(), '') self.assertTrue(f.is_valid()) - f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')}, auto_id=False) + f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', u'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह'.encode('utf-8'))}, auto_id=False) self.assertHTMLEqual(f.as_table(), '') def test_basic_processing_in_view(self): diff --git a/tests/regressiontests/forms/tests/models.py b/tests/regressiontests/forms/tests/models.py index 64cd18bde6..7f569651c7 100644 --- a/tests/regressiontests/forms/tests/models.py +++ b/tests/regressiontests/forms/tests/models.py @@ -106,7 +106,7 @@ class ModelFormCallableModelDefault(TestCase): class FormsModelTestCase(TestCase): def test_unicode_filename(self): # FileModel with unicode filename and data ######################### - f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')}, auto_id=False) + f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', u'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह'.encode('utf-8'))}, auto_id=False) self.assertTrue(f.is_valid()) self.assertTrue('file1' in f.cleaned_data) m = FileModel.objects.create(file=f.cleaned_data['file1']) @@ -177,7 +177,7 @@ class RelatedModelFormTests(TestCase): class Meta: model=A - self.assertRaises(ValueError, ModelFormMetaclass, 'Form', (ModelForm,), {'Meta': Meta}) + self.assertRaises(ValueError, ModelFormMetaclass, b'Form', (ModelForm,), {'Meta': Meta}) class B(models.Model): pass @@ -195,4 +195,4 @@ class RelatedModelFormTests(TestCase): class Meta: model=A - self.assertTrue(issubclass(ModelFormMetaclass('Form', (ModelForm,), {'Meta': Meta}), ModelForm)) + self.assertTrue(issubclass(ModelFormMetaclass(b'Form', (ModelForm,), {'Meta': Meta}), ModelForm)) diff --git a/tests/regressiontests/forms/tests/regressions.py b/tests/regressiontests/forms/tests/regressions.py index 267ef300e6..20c11f99a8 100644 --- a/tests/regressiontests/forms/tests/regressions.py +++ b/tests/regressiontests/forms/tests/regressions.py @@ -54,10 +54,11 @@ class FormsRegressionsTestCase(TestCase): # Testing choice validation with UTF-8 bytestrings as input (these are the # Russian abbreviations "мес." and "шт.". - UNITS = (('\xd0\xbc\xd0\xb5\xd1\x81.', '\xd0\xbc\xd0\xb5\xd1\x81.'), ('\xd1\x88\xd1\x82.', '\xd1\x88\xd1\x82.')) + UNITS = ((b'\xd0\xbc\xd0\xb5\xd1\x81.', b'\xd0\xbc\xd0\xb5\xd1\x81.'), + (b'\xd1\x88\xd1\x82.', b'\xd1\x88\xd1\x82.')) f = ChoiceField(choices=UNITS) self.assertEqual(f.clean(u'\u0448\u0442.'), u'\u0448\u0442.') - self.assertEqual(f.clean('\xd1\x88\xd1\x82.'), u'\u0448\u0442.') + self.assertEqual(f.clean(b'\xd1\x88\xd1\x82.'), u'\u0448\u0442.') # Translated error messages used to be buggy. with override('ru'): diff --git a/tests/regressiontests/forms/tests/widgets.py b/tests/regressiontests/forms/tests/widgets.py index 2499b7a350..8630b45666 100644 --- a/tests/regressiontests/forms/tests/widgets.py +++ b/tests/regressiontests/forms/tests/widgets.py @@ -1182,7 +1182,7 @@ class ClearableFileInputTests(TestCase): """ widget = ClearableFileInput() widget.is_required = True - f = SimpleUploadedFile('something.txt', 'content') + f = SimpleUploadedFile('something.txt', b'content') self.assertEqual(widget.value_from_datadict( data={'myfile-clear': True}, files={'myfile': f}, diff --git a/tests/regressiontests/generic_views/base.py b/tests/regressiontests/generic_views/base.py index e18ed2a2a7..fc3dc05fd7 100644 --- a/tests/regressiontests/generic_views/base.py +++ b/tests/regressiontests/generic_views/base.py @@ -173,6 +173,58 @@ class ViewTest(unittest.TestCase): """ self.assertTrue(DecoratedDispatchView.as_view().is_decorated) + def test_head_no_get(self): + """ + Test that a view class with no get responds to a HEAD request with HTTP + 405. + """ + request = self.rf.head('/') + view = PostOnlyView.as_view() + self.assertEqual(405, view(request).status_code) + + def test_options(self): + """ + Test that views respond to HTTP OPTIONS requests with an Allow header + appropriate for the methods implemented by the view class. + """ + request = self.rf.options('/') + view = SimpleView.as_view() + response = view(request) + self.assertEqual(200, response.status_code) + self.assertTrue(response['Allow']) + + def test_options_for_get_view(self): + """ + Test that a view implementing GET allows GET and HEAD. + """ + request = self.rf.options('/') + view = SimpleView.as_view() + response = view(request) + self._assert_allows(response, 'GET', 'HEAD') + + def test_options_for_get_and_post_view(self): + """ + Test that a view implementing GET and POST allows GET, HEAD, and POST. + """ + request = self.rf.options('/') + view = SimplePostView.as_view() + response = view(request) + self._assert_allows(response, 'GET', 'HEAD', 'POST') + + def test_options_for_post_view(self): + """ + Test that a view implementing POST allows POST. + """ + request = self.rf.options('/') + view = PostOnlyView.as_view() + response = view(request) + self._assert_allows(response, 'POST') + + def _assert_allows(self, response, *expected_methods): + "Assert allowed HTTP methods reported in the Allow response header" + response_allows = set(response['Allow'].split(', ')) + self.assertEqual(set(expected_methods + ('OPTIONS',)), response_allows) + class TemplateViewTest(TestCase): urls = 'regressiontests.generic_views.urls' diff --git a/tests/regressiontests/generic_views/dates.py b/tests/regressiontests/generic_views/dates.py index 98c089f2b4..ad65fd9f10 100644 --- a/tests/regressiontests/generic_views/dates.py +++ b/tests/regressiontests/generic_views/dates.py @@ -86,10 +86,15 @@ class ArchiveIndexViewTests(TestCase): # 1 query for years list + 1 query for books with self.assertNumQueries(2): self.client.get('/dates/books/') - # same as above + 1 query to test if books exist - with self.assertNumQueries(3): + # same as above + 1 query to test if books exist + 1 query to count them + with self.assertNumQueries(4): self.client.get('/dates/books/paginated/') + def test_no_duplicate_query(self): + # Regression test for #18354 + with self.assertNumQueries(2): + self.client.get('/dates/books/reverse/') + def test_datetime_archive_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/') @@ -110,9 +115,13 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/books/2008/') self.assertEqual(res.status_code, 200) self.assertEqual(list(res.context['date_list']), [datetime.datetime(2008, 10, 1)]) - self.assertEqual(res.context['year'], '2008') + self.assertEqual(res.context['year'], datetime.date(2008, 1, 1)) self.assertTemplateUsed(res, 'generic_views/book_archive_year.html') + # Since allow_empty=False, next/prev years must be valid (#7164) + self.assertEqual(res.context['next_year'], None) + self.assertEqual(res.context['previous_year'], datetime.date(2006, 1, 1)) + def test_year_view_make_object_list(self): res = self.client.get('/dates/books/2006/make_object_list/') self.assertEqual(res.status_code, 200) @@ -129,6 +138,10 @@ class YearArchiveViewTests(TestCase): self.assertEqual(list(res.context['date_list']), []) self.assertEqual(list(res.context['book_list']), []) + # Since allow_empty=True, next/prev are allowed to be empty years (#7164) + self.assertEqual(res.context['next_year'], datetime.date(2000, 1, 1)) + self.assertEqual(res.context['previous_year'], datetime.date(1998, 1, 1)) + def test_year_view_allow_future(self): # Create a new book in the future year = datetime.date.today().year + 1 @@ -155,6 +168,11 @@ class YearArchiveViewTests(TestCase): res = self.client.get('/dates/books/no_year/') self.assertEqual(res.status_code, 404) + def test_no_duplicate_query(self): + # Regression test for #18354 + with self.assertNumQueries(4): + self.client.get('/dates/books/2008/reverse/') + def test_datetime_year_view(self): BookSigning.objects.create(event_date=datetime.datetime(2008, 4, 2, 12, 0)) res = self.client.get('/dates/booksignings/2008/') @@ -446,6 +464,13 @@ class DayArchiveViewTests(TestCase): self.assertEqual(res.context['next_day'], future) self.assertEqual(res.context['previous_day'], datetime.date(2006, 5, 1)) + # allow_future for yesterday, next_day is today (#17192) + today = datetime.date.today() + yesterday = today - datetime.timedelta(days=1) + res = self.client.get('/dates/books/%s/allow_empty_and_future/' + % yesterday.strftime('%Y/%b/%d').lower()) + self.assertEqual(res.context['next_day'], today) + def test_day_view_paginated(self): res = self.client.get('/dates/books/2008/oct/1/') self.assertEqual(res.status_code, 200) diff --git a/tests/regressiontests/generic_views/list.py b/tests/regressiontests/generic_views/list.py index 9ad00edc3b..b925758524 100644 --- a/tests/regressiontests/generic_views/list.py +++ b/tests/regressiontests/generic_views/list.py @@ -159,6 +159,16 @@ class ListViewTests(TestCase): def test_missing_items(self): self.assertRaises(ImproperlyConfigured, self.client.get, '/list/authors/invalid/') + def test_paginated_list_view_does_not_load_entire_table(self): + # Regression test for #17535 + self._make_authors(3) + # 1 query for authors + with self.assertNumQueries(1): + self.client.get('/list/authors/notempty/') + # same as above + 1 query to test if authors exist + 1 query for pagination + with self.assertNumQueries(3): + self.client.get('/list/authors/notempty/paginated/') + def _make_authors(self, n): Author.objects.all().delete() for i in range(n): diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_archive.html b/tests/regressiontests/generic_views/templates/generic_views/book_archive.html index 5fc11149bd..03f45a38c2 100644 --- a/tests/regressiontests/generic_views/templates/generic_views/book_archive.html +++ b/tests/regressiontests/generic_views/templates/generic_views/book_archive.html @@ -1 +1 @@ -Archive of books from {{ date_list }}. \ No newline at end of file +Archive of books from {{ date_list }}. {{ object_list|length }} books found. diff --git a/tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html b/tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html index 7705914e97..fde198dcaf 100644 --- a/tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html +++ b/tests/regressiontests/generic_views/templates/generic_views/book_archive_year.html @@ -1 +1 @@ -Archive of books from {{ year }}. \ No newline at end of file +Archive of books from {{ year }}. {{ object_list|length }} books found. diff --git a/tests/regressiontests/generic_views/urls.py b/tests/regressiontests/generic_views/urls.py index 9c47ab8d82..14d4f685a6 100644 --- a/tests/regressiontests/generic_views/urls.py +++ b/tests/regressiontests/generic_views/urls.py @@ -4,6 +4,7 @@ from django.conf.urls import patterns, url from django.views.decorators.cache import cache_page from django.views.generic import TemplateView +from . import models from . import views @@ -108,6 +109,8 @@ urlpatterns = patterns('', views.BookArchive.as_view(queryset=None)), (r'^dates/books/paginated/$', views.BookArchive.as_view(paginate_by=10)), + (r'^dates/books/reverse/$', + views.BookArchive.as_view(queryset=models.Book.objects.order_by('pubdate'))), (r'^dates/booksignings/$', views.BookSigningArchive.as_view()), @@ -128,6 +131,8 @@ urlpatterns = patterns('', views.AuthorList.as_view(paginate_by=30)), (r'^list/authors/notempty/$', views.AuthorList.as_view(allow_empty=False)), + (r'^list/authors/notempty/paginated/$', + views.AuthorList.as_view(allow_empty=False, paginate_by=2)), (r'^list/authors/template_name/$', views.AuthorList.as_view(template_name='generic_views/list.html')), (r'^list/authors/template_name_suffix/$', @@ -158,6 +163,8 @@ urlpatterns = patterns('', views.BookYearArchive.as_view(make_object_list=True, paginate_by=30)), (r'^dates/books/no_year/$', views.BookYearArchive.as_view()), + (r'^dates/books/(?P\d{4})/reverse/$', + views.BookYearArchive.as_view(queryset=models.Book.objects.order_by('pubdate'))), (r'^dates/booksignings/(?P\d{4})/$', views.BookSigningYearArchive.as_view()), @@ -202,6 +209,8 @@ urlpatterns = patterns('', views.BookDayArchive.as_view(allow_empty=True)), (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/allow_future/$', views.BookDayArchive.as_view(allow_future=True)), + (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/allow_empty_and_future/$', + views.BookDayArchive.as_view(allow_empty=True, allow_future=True)), (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/(?P\d{1,2})/paginated/$', views.BookDayArchive.as_view(paginate_by=True)), (r'^dates/books/(?P\d{4})/(?P[a-z]{3})/no_day/$', diff --git a/tests/regressiontests/handlers/tests.py b/tests/regressiontests/handlers/tests.py index 59d3d84528..ae2062c756 100644 --- a/tests/regressiontests/handlers/tests.py +++ b/tests/regressiontests/handlers/tests.py @@ -28,7 +28,7 @@ class HandlerTests(unittest.TestCase): def test_bad_path_info(self): """Tests for bug #15672 ('request' referenced before assignment)""" environ = RequestFactory().get('/').environ - environ['PATH_INFO'] = '\xed' + environ['PATH_INFO'] = b'\xed' handler = WSGIHandler() response = handler(environ, lambda *a, **k: None) self.assertEqual(response.status_code, 400) diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 6dd2128afd..218c5b5b3b 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -174,7 +174,7 @@ class QueryDictTests(unittest.TestCase): QueryDicts must be able to handle invalid input encoding (in this case, bad UTF-8 encoding). """ - q = QueryDict('foo=bar&foo=\xff') + q = QueryDict(b'foo=bar&foo=\xff') self.assertEqual(q['foo'], u'\ufffd') self.assertEqual(q.getlist('foo'), [u'bar', u'\ufffd']) @@ -198,7 +198,7 @@ class QueryDictTests(unittest.TestCase): def test_non_default_encoding(self): """#13572 - QueryDict with a non-default encoding""" - q = QueryDict('sbb=one', encoding='rot_13') + q = QueryDict(b'sbb=one', encoding='rot_13') self.assertEqual(q.encoding , 'rot_13' ) self.assertEqual(q.items() , [(u'foo', u'bar')] ) self.assertEqual(q.urlencode() , 'sbb=one' ) @@ -285,8 +285,8 @@ class HttpResponseTests(unittest.TestCase): except StopIteration: break #'\xde\x9e' == unichr(1950).encode('utf-8') - self.assertEqual(result, ['1', '2', '3', '\xde\x9e']) - self.assertEqual(r.content, '123\xde\x9e') + self.assertEqual(result, ['1', '2', '3', b'\xde\x9e']) + self.assertEqual(r.content, b'123\xde\x9e') #with Content-Encoding header r = HttpResponse([1,1,2,4,8]) @@ -321,7 +321,7 @@ class CookieTests(unittest.TestCase): Test that we haven't broken normal encoding """ c = SimpleCookie() - c['test'] = "\xf0" + c['test'] = b"\xf0" c2 = SimpleCookie() c2.load(c.output()) self.assertEqual(c['test'].value, c2['test'].value) diff --git a/tests/regressiontests/i18n/commands/compilation.py b/tests/regressiontests/i18n/commands/compilation.py index 039e463d0b..d88e1feef6 100644 --- a/tests/regressiontests/i18n/commands/compilation.py +++ b/tests/regressiontests/i18n/commands/compilation.py @@ -1,8 +1,7 @@ import os from io import BytesIO -from django.core.management import CommandError -from django.core.management.commands.compilemessages import compile_messages +from django.core.management import call_command, CommandError from django.test import TestCase from django.test.utils import override_settings from django.utils import translation @@ -25,11 +24,9 @@ class PoFileTests(MessageCompilationTests): def test_bom_rejection(self): os.chdir(test_dir) - # We don't use the django.core.management infrastructure (call_command() - # et al) because CommandError's cause exit(1) there. We test the - # underlying compile_messages function instead - out = BytesIO() - self.assertRaises(CommandError, compile_messages, out, locale=self.LOCALE) + with self.assertRaisesRegexp(CommandError, + "file has a BOM \(Byte Order Mark\)"): + call_command('compilemessages', locale=self.LOCALE, stderr=BytesIO()) self.assertFalse(os.path.exists(self.MO_FILE)) @@ -45,11 +42,7 @@ class PoFileContentsTests(MessageCompilationTests): def test_percent_symbol_in_po_file(self): os.chdir(test_dir) - # We don't use the django.core.management infrastructure (call_command() - # et al) because CommandError's cause exit(1) there. We test the - # underlying compile_messages function instead - out = BytesIO() - compile_messages(out, locale=self.LOCALE) + call_command('compilemessages', locale=self.LOCALE, stderr=BytesIO()) self.assertTrue(os.path.exists(self.MO_FILE)) @@ -64,11 +57,7 @@ class PercentRenderingTests(MessageCompilationTests): def test_percent_symbol_escaping(self): from django.template import Template, Context os.chdir(test_dir) - # We don't use the django.core.management infrastructure (call_command() - # et al) because CommandError's cause exit(1) there. We test the - # underlying compile_messages function instead - out = BytesIO() - compile_messages(out, locale=self.LOCALE) + call_command('compilemessages', locale=self.LOCALE, stderr=BytesIO()) with translation.override(self.LOCALE): t = Template('{% load i18n %}{% trans "Looks like a str fmt spec %% o but shouldn\'t be interpreted as such" %}') rendered = t.render(Context({})) diff --git a/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.mo b/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.mo index f0a2179767..478338bc88 100644 Binary files a/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.mo and b/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.mo differ diff --git a/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.po b/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.po index 8e9b9e1034..dafb6139ae 100644 --- a/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.po +++ b/tests/regressiontests/i18n/other/locale/fr/LC_MESSAGES/django.po @@ -17,5 +17,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #: template.html:3 +# Note: Intentional: variable name is translated. msgid "My name is %(person)s." msgstr "Mon nom est %(personne)s." + +#: template.html:3 +# Note: Intentional: the variable name is badly formatted (missing 's' at the end) +msgid "My other name is %(person)s." +msgstr "Mon autre nom est %(person)." \ No newline at end of file diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index faed4e55c7..78e41321af 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -230,7 +230,7 @@ class TranslationTests(TestCase): """ Translating a string requiring no auto-escaping shouldn't change the "safe" status. """ - s = mark_safe('Password') + s = mark_safe(b'Password') self.assertEqual(SafeString, type(s)) with translation.override('de', deactivate=True): self.assertEqual(SafeUnicode, type(ugettext(s))) @@ -270,10 +270,11 @@ class TranslationTests(TestCase): self.assertEqual(to_language('sr_Lat'), 'sr-lat') @override_settings(LOCALE_PATHS=(os.path.join(here, 'other', 'locale'),)) - def test_bad_placeholder(self): + def test_bad_placeholder_1(self): """ Error in translation file should not crash template rendering (%(person)s is translated as %(personne)s in fr.po) + Refs #16516. """ from django.template import Template, Context with translation.override('fr'): @@ -281,6 +282,19 @@ class TranslationTests(TestCase): rendered = t.render(Context({'person': 'James'})) self.assertEqual(rendered, 'My name is James.') + @override_settings(LOCALE_PATHS=(os.path.join(here, 'other', 'locale'),)) + def test_bad_placeholder_2(self): + """ + Error in translation file should not crash template rendering + (%(person) misses a 's' in fr.po, causing the string formatting to fail) + Refs #18393. + """ + from django.template import Template, Context + with translation.override('fr'): + t = Template('{% load i18n %}{% blocktrans %}My other name is {{ person }}.{% endblocktrans %}') + rendered = t.render(Context({'person': 'James'})) + self.assertEqual(rendered, 'My other name is James.') + class FormattingTests(TestCase): diff --git a/tests/regressiontests/inspectdb/tests.py b/tests/regressiontests/inspectdb/tests.py index 6896bf9126..29435b8375 100644 --- a/tests/regressiontests/inspectdb/tests.py +++ b/tests/regressiontests/inspectdb/tests.py @@ -6,10 +6,28 @@ from django.test import TestCase, skipUnlessDBFeature class InspectDBTestCase(TestCase): + def test_stealth_table_name_filter_option(self): + out = StringIO() + # Lets limit the introspection to tables created for models of this + # application + call_command('inspectdb', + table_name_filter=lambda tn:tn.startswith('inspectdb_'), + stdout=out) + error_message = "inspectdb has examined a table that should have been filtered out." + # contrib.contenttypes is one of the apps always installed when running + # the Django test suite, check that one of its tables hasn't been + # inspected + self.assertNotIn("class DjangoContentType(models.Model):", out.getvalue(), msg=error_message) + out.close() + @skipUnlessDBFeature('can_introspect_foreign_keys') def test_attribute_name_not_python_keyword(self): out = StringIO() - call_command('inspectdb', stdout=out) + # Lets limit the introspection to tables created for models of this + # application + call_command('inspectdb', + table_name_filter=lambda tn:tn.startswith('inspectdb_'), + stdout=out) error_message = "inspectdb generated an attribute name which is a python keyword" self.assertNotIn("from = models.ForeignKey(InspectdbPeople)", out.getvalue(), msg=error_message) # As InspectdbPeople model is defined after InspectdbMessage, it should be quoted @@ -23,7 +41,11 @@ class InspectDBTestCase(TestCase): def test_digits_column_name_introspection(self): """Introspection of column names consist/start with digits (#16536/#17676)""" out = StringIO() - call_command('inspectdb', stdout=out) + # Lets limit the introspection to tables created for models of this + # application + call_command('inspectdb', + table_name_filter=lambda tn:tn.startswith('inspectdb_'), + stdout=out) error_message = "inspectdb generated a model field name which is a number" self.assertNotIn(" 123 = models.CharField", out.getvalue(), msg=error_message) self.assertIn("number_123 = models.CharField", out.getvalue()) diff --git a/tests/regressiontests/localflavor/es/tests.py b/tests/regressiontests/localflavor/es/tests.py index c989e3e9ca..c13eac6d61 100644 --- a/tests/regressiontests/localflavor/es/tests.py +++ b/tests/regressiontests/localflavor/es/tests.py @@ -31,7 +31,7 @@ class ESLocalFlavorTests(SimpleTestCase): def test_ESProvinceSelect(self): f = ESProvinceSelect() out = u''' diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index a4cefd8322..85e73c197f 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -86,7 +86,7 @@ class MailTests(TestCase): """ headers = {"date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"} email = EmailMessage('subject', 'content', 'from@example.com', ['to@example.com'], headers=headers) - self.assertEqual(email.message().as_string(), 'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: subject\nFrom: from@example.com\nTo: to@example.com\ndate: Fri, 09 Nov 2001 01:08:47 -0000\nMessage-ID: foo\n\ncontent') + self.assertEqual(email.message().as_string(), b'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: subject\nFrom: from@example.com\nTo: to@example.com\ndate: Fri, 09 Nov 2001 01:08:47 -0000\nMessage-ID: foo\n\ncontent') def test_from_header(self): """ @@ -168,7 +168,7 @@ class MailTests(TestCase): email = EmailMessage('Subject', 'Firstname Sürname is a great guy.', 'from@example.com', ['other@example.com']) email.encoding = 'iso-8859-1' message = email.message() - self.assertTrue(message.as_string().startswith('Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: from@example.com\nTo: other@example.com')) + self.assertTrue(message.as_string().startswith(b'Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: from@example.com\nTo: other@example.com')) self.assertEqual(message.get_payload(), 'Firstname S=FCrname is a great guy.') # Make sure MIME attachments also works correctly with other encodings than utf-8 @@ -177,8 +177,8 @@ class MailTests(TestCase): msg = EmailMultiAlternatives('Subject', text_content, 'from@example.com', ['to@example.com']) msg.encoding = 'iso-8859-1' msg.attach_alternative(html_content, "text/html") - self.assertEqual(msg.message().get_payload(0).as_string(), 'Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\nFirstname S=FCrname is a great guy.') - self.assertEqual(msg.message().get_payload(1).as_string(), 'Content-Type: text/html; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\n

    Firstname S=FCrname is a great guy.

    ') + self.assertEqual(msg.message().get_payload(0).as_string(), b'Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\nFirstname S=FCrname is a great guy.') + self.assertEqual(msg.message().get_payload(1).as_string(), b'Content-Type: text/html; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\n

    Firstname S=FCrname is a great guy.

    ') def test_attachments(self): """Regression test for #9367""" @@ -188,7 +188,7 @@ class MailTests(TestCase): html_content = '

    This is an important message.

    ' msg = EmailMultiAlternatives(subject, text_content, from_email, [to], headers=headers) msg.attach_alternative(html_content, "text/html") - msg.attach("an attachment.pdf", "%PDF-1.4.%...", mimetype="application/pdf") + msg.attach("an attachment.pdf", b"%PDF-1.4.%...", mimetype="application/pdf") msg_str = msg.message().as_string() message = email.message_from_string(msg_str) self.assertTrue(message.is_multipart()) @@ -205,7 +205,7 @@ class MailTests(TestCase): content = 'This is the message.' msg = EmailMessage(subject, content, from_email, [to], headers=headers) # Unicode in file name - msg.attach(u"une pièce jointe.pdf", "%PDF-1.4.%...", mimetype="application/pdf") + msg.attach(u"une pièce jointe.pdf", b"%PDF-1.4.%...", mimetype="application/pdf") msg_str = msg.message().as_string() message = email.message_from_string(msg_str) payload = message.get_payload() @@ -289,31 +289,31 @@ class MailTests(TestCase): # Regression for #13433 - Make sure that EmailMessage doesn't mangle # 'From ' in message body. email = EmailMessage('Subject', 'From the future', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) - self.assertFalse('>From the future' in email.message().as_string()) + self.assertFalse(b'>From the future' in email.message().as_string()) def test_dont_base64_encode(self): # Ticket #3472 # Shouldn't use Base64 encoding at all msg = EmailMessage('Subject', 'UTF-8 encoded body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) - self.assertFalse('Content-Transfer-Encoding: base64' in msg.message().as_string()) + self.assertFalse(b'Content-Transfer-Encoding: base64' in msg.message().as_string()) # Ticket #11212 # Shouldn't use quoted printable, should detect it can represent content with 7 bit data msg = EmailMessage('Subject', 'Body with only ASCII characters.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) s = msg.message().as_string() - self.assertFalse('Content-Transfer-Encoding: quoted-printable' in s) - self.assertTrue('Content-Transfer-Encoding: 7bit' in s) + self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' in s) + self.assertTrue(b'Content-Transfer-Encoding: 7bit' in s) # Shouldn't use quoted printable, should detect it can represent content with 8 bit data msg = EmailMessage('Subject', 'Body with latin characters: àáä.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) s = msg.message().as_string() - self.assertFalse('Content-Transfer-Encoding: quoted-printable' in s) - self.assertTrue('Content-Transfer-Encoding: 8bit' in s) + self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' in s) + self.assertTrue(b'Content-Transfer-Encoding: 8bit' in s) msg = EmailMessage('Subject', u'Body with non latin characters: А Б В Г Д Е Ж Ѕ З И І К Л М Н О П.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) s = msg.message().as_string() - self.assertFalse('Content-Transfer-Encoding: quoted-printable' in s) - self.assertTrue('Content-Transfer-Encoding: 8bit' in s) + self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' in s) + self.assertTrue(b'Content-Transfer-Encoding: 8bit' in s) class BaseEmailBackendTests(object): @@ -438,7 +438,7 @@ class BaseEmailBackendTests(object): email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'], cc=['cc@example.com']) mail.get_connection().send_messages([email]) message = self.get_the_message() - self.assertStartsWith(message.as_string(), 'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nCc: cc@example.com\nDate: ') + self.assertStartsWith(message.as_string(), b'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nCc: cc@example.com\nDate: ') def test_idn_send(self): """ @@ -500,14 +500,14 @@ class FileBackendTests(BaseEmailBackendTests, TestCase): email_backend = 'django.core.mail.backends.filebased.EmailBackend' def setUp(self): + super(FileBackendTests, self).setUp() self.tmp_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self.tmp_dir) - self.settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir) - self.settings_override.enable() - super(FileBackendTests, self).setUp() + self._settings_override = override_settings(EMAIL_FILE_PATH=self.tmp_dir) + self._settings_override.enable() def tearDown(self): - self.settings_override.disable() + self._settings_override.disable() super(FileBackendTests, self).tearDown() def flush_mailbox(self): @@ -517,8 +517,8 @@ class FileBackendTests(BaseEmailBackendTests, TestCase): def get_mailbox_content(self): messages = [] for filename in os.listdir(self.tmp_dir): - with open(os.path.join(self.tmp_dir, filename)) as fp: - session = fp.read().split('\n' + ('-' * 79) + '\n') + with open(os.path.join(self.tmp_dir, filename), 'rb') as fp: + session = fp.read().split(b'\n' + (b'-' * 79) + b'\n') messages.extend(email.message_from_string(m) for m in session if m) return messages @@ -569,7 +569,7 @@ class ConsoleBackendTests(BaseEmailBackendTests, TestCase): self.stream = sys.stdout = StringIO() def get_mailbox_content(self): - messages = self.stream.getvalue().split('\n' + ('-' * 79) + '\n') + messages = self.stream.getvalue().split(b'\n' + (b'-' * 79) + b'\n') return [email.message_from_string(m) for m in messages if m] def test_console_stream_kwarg(self): @@ -644,15 +644,15 @@ class SMTPBackendTests(BaseEmailBackendTests, TestCase): @classmethod def setUpClass(cls): cls.server = FakeSMTPServer(('127.0.0.1', 0), None) - cls.settings_override = override_settings( + cls._settings_override = override_settings( EMAIL_HOST="127.0.0.1", EMAIL_PORT=cls.server.socket.getsockname()[1]) - cls.settings_override.enable() + cls._settings_override.enable() cls.server.start() @classmethod def tearDownClass(cls): - cls.settings_override.disable() + cls._settings_override.disable() cls.server.stop() def setUp(self): diff --git a/tests/regressiontests/model_fields/tests.py b/tests/regressiontests/model_fields/tests.py index ea1e1c7d99..9c0f888a99 100644 --- a/tests/regressiontests/model_fields/tests.py +++ b/tests/regressiontests/model_fields/tests.py @@ -208,7 +208,7 @@ class BooleanFieldTests(unittest.TestCase): # Verify that when an extra clause exists, the boolean # conversions are applied with an offset b5 = BooleanModel.objects.all().extra( - select={'string_length': 'LENGTH(string)'})[0] + select={'string_col': 'string'})[0] self.assertFalse(isinstance(b5.pk, bool)) class ChoicesTests(test.TestCase): @@ -365,3 +365,15 @@ class FileFieldTests(unittest.TestCase): field = d._meta.get_field('myfile') field.save_form_data(d, 'else.txt') self.assertEqual(d.myfile, 'else.txt') + + def test_max_length(self): + """ + Test that FileField validates the length of the generated file name + that will be stored in the database. Regression for #9893. + + """ + # upload_to = 'unused', so file names are saved as 'unused/xxxxx'. + # max_length = 100, so names longer than 93 characters are rejected. + Document(myfile=93 * 'x').full_clean() + with self.assertRaises(ValidationError): + Document(myfile=94 * 'x').full_clean() diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py index 09da9737a8..d33f52af9a 100644 --- a/tests/regressiontests/model_forms_regress/tests.py +++ b/tests/regressiontests/model_forms_regress/tests.py @@ -394,7 +394,7 @@ class FileFieldTests(unittest.TestCase): form = DocumentForm() self.assertTrue('name="myfile"' in unicode(form)) self.assertTrue('myfile-clear' not in unicode(form)) - form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', 'content')}) + form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', b'content')}) self.assertTrue(form.is_valid()) doc = form.save(commit=False) self.assertEqual(doc.myfile.name, 'something.txt') @@ -411,11 +411,11 @@ class FileFieldTests(unittest.TestCase): includes the current file and the clear checkbox. """ - form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', 'content')}) + form = DocumentForm(files={'myfile': SimpleUploadedFile('something.txt', b'content')}) self.assertTrue(form.is_valid()) doc = form.save(commit=False) form = DocumentForm(instance=doc, - files={'myfile': SimpleUploadedFile('something.txt', 'content')}, + files={'myfile': SimpleUploadedFile('something.txt', b'content')}, data={'myfile-clear': 'true'}) self.assertTrue(not form.is_valid()) self.assertEqual(form.errors['myfile'], diff --git a/tests/regressiontests/queries/models.py b/tests/regressiontests/queries/models.py index 07aa6db67b..e4ce9d7b60 100644 --- a/tests/regressiontests/queries/models.py +++ b/tests/regressiontests/queries/models.py @@ -264,7 +264,7 @@ class SingleObject(models.Model): return self.name class RelatedObject(models.Model): - single = models.ForeignKey(SingleObject) + single = models.ForeignKey(SingleObject, null=True) class Meta: ordering = ['single'] diff --git a/tests/regressiontests/queries/tests.py b/tests/regressiontests/queries/tests.py index 14a693f2c5..1035a38840 100644 --- a/tests/regressiontests/queries/tests.py +++ b/tests/regressiontests/queries/tests.py @@ -19,7 +19,8 @@ from .models import (Annotation, Article, Author, Celebrity, Child, Cover, ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, - SpecialCategory, OneToOneCategory, NullableName, ProxyCategory) + SpecialCategory, OneToOneCategory, NullableName, ProxyCategory, + SingleObject, RelatedObject) class BaseQuerysetTest(TestCase): @@ -843,6 +844,39 @@ class Queries1Tests(BaseQuerysetTest): ) Tag._meta.ordering = original_ordering + def test_exclude(self): + self.assertQuerysetEqual( + Item.objects.exclude(tags__name='t4'), + [repr(i) for i in Item.objects.filter(~Q(tags__name='t4'))]) + self.assertQuerysetEqual( + Item.objects.exclude(Q(tags__name='t4')|Q(tags__name='t3')), + [repr(i) for i in Item.objects.filter(~(Q(tags__name='t4')|Q(tags__name='t3')))]) + self.assertQuerysetEqual( + Item.objects.exclude(Q(tags__name='t4')|~Q(tags__name='t3')), + [repr(i) for i in Item.objects.filter(~(Q(tags__name='t4')|~Q(tags__name='t3')))]) + + def test_nested_exclude(self): + self.assertQuerysetEqual( + Item.objects.exclude(~Q(tags__name='t4')), + [repr(i) for i in Item.objects.filter(~~Q(tags__name='t4'))]) + + def test_double_exclude(self): + self.assertQuerysetEqual( + Item.objects.filter(Q(tags__name='t4')), + [repr(i) for i in Item.objects.filter(~~Q(tags__name='t4'))]) + self.assertQuerysetEqual( + Item.objects.filter(Q(tags__name='t4')), + [repr(i) for i in Item.objects.filter(~Q(~Q(tags__name='t4')))]) + + @unittest.expectedFailure + def test_exclude_in(self): + self.assertQuerysetEqual( + Item.objects.exclude(Q(tags__name__in=['t4', 't3'])), + [repr(i) for i in Item.objects.filter(~Q(tags__name__in=['t4', 't3']))]) + self.assertQuerysetEqual( + Item.objects.filter(Q(tags__name__in=['t4', 't3'])), + [repr(i) for i in Item.objects.filter(~~Q(tags__name__in=['t4', 't3']))]) + class Queries2Tests(TestCase): def setUp(self): Number.objects.create(num=4) @@ -1321,12 +1355,33 @@ class NullableRelOrderingTests(TestCase): def test_ticket10028(self): # Ordering by model related to nullable relations(!) should use outer # joins, so that all results are included. - _ = Plaything.objects.create(name="p1") + Plaything.objects.create(name="p1") self.assertQuerysetEqual( Plaything.objects.all(), [''] ) + def test_join_already_in_query(self): + # Ordering by model related to nullable relations should not change + # the join type of already existing joins. + Plaything.objects.create(name="p1") + s = SingleObject.objects.create(name='s') + r = RelatedObject.objects.create(single=s) + Plaything.objects.create(name="p2", others=r) + qs = Plaything.objects.all().filter(others__isnull=False).order_by('pk') + self.assertTrue('INNER' in str(qs.query)) + qs = qs.order_by('others__single__name') + # The ordering by others__single__pk will add one new join (to single) + # and that join must be LEFT join. The already existing join to related + # objects must be kept INNER. So, we have both a INNER and a LEFT join + # in the query. + self.assertTrue('LEFT' in str(qs.query)) + self.assertTrue('INNER' in str(qs.query)) + self.assertQuerysetEqual( + qs, + [''] + ) + class DisjunctiveFilterTests(TestCase): def setUp(self): diff --git a/tests/regressiontests/requests/tests.py b/tests/regressiontests/requests/tests.py index 5c49afbc35..3d2b196194 100644 --- a/tests/regressiontests/requests/tests.py +++ b/tests/regressiontests/requests/tests.py @@ -204,91 +204,91 @@ class RequestsTests(unittest.TestCase): def test_limited_stream(self): # Read all of a limited stream - stream = LimitedStream(StringIO('test'), 2) - self.assertEqual(stream.read(), 'te') + stream = LimitedStream(StringIO(b'test'), 2) + self.assertEqual(stream.read(), b'te') # Reading again returns nothing. - self.assertEqual(stream.read(), '') + self.assertEqual(stream.read(), b'') # Read a number of characters greater than the stream has to offer - stream = LimitedStream(StringIO('test'), 2) - self.assertEqual(stream.read(5), 'te') + stream = LimitedStream(StringIO(b'test'), 2) + self.assertEqual(stream.read(5), b'te') # Reading again returns nothing. - self.assertEqual(stream.readline(5), '') + self.assertEqual(stream.readline(5), b'') # Read sequentially from a stream - stream = LimitedStream(StringIO('12345678'), 8) - self.assertEqual(stream.read(5), '12345') - self.assertEqual(stream.read(5), '678') + stream = LimitedStream(StringIO(b'12345678'), 8) + self.assertEqual(stream.read(5), b'12345') + self.assertEqual(stream.read(5), b'678') # Reading again returns nothing. - self.assertEqual(stream.readline(5), '') + self.assertEqual(stream.readline(5), b'') # Read lines from a stream - stream = LimitedStream(StringIO('1234\n5678\nabcd\nefgh\nijkl'), 24) + stream = LimitedStream(StringIO(b'1234\n5678\nabcd\nefgh\nijkl'), 24) # Read a full line, unconditionally - self.assertEqual(stream.readline(), '1234\n') + self.assertEqual(stream.readline(), b'1234\n') # Read a number of characters less than a line - self.assertEqual(stream.readline(2), '56') + self.assertEqual(stream.readline(2), b'56') # Read the rest of the partial line - self.assertEqual(stream.readline(), '78\n') + self.assertEqual(stream.readline(), b'78\n') # Read a full line, with a character limit greater than the line length - self.assertEqual(stream.readline(6), 'abcd\n') + self.assertEqual(stream.readline(6), b'abcd\n') # Read the next line, deliberately terminated at the line end - self.assertEqual(stream.readline(4), 'efgh') + self.assertEqual(stream.readline(4), b'efgh') # Read the next line... just the line end - self.assertEqual(stream.readline(), '\n') + self.assertEqual(stream.readline(), b'\n') # Read everything else. - self.assertEqual(stream.readline(), 'ijkl') + self.assertEqual(stream.readline(), b'ijkl') # Regression for #15018 # If a stream contains a newline, but the provided length # is less than the number of provided characters, the newline # doesn't reset the available character count - stream = LimitedStream(StringIO('1234\nabcdef'), 9) - self.assertEqual(stream.readline(10), '1234\n') - self.assertEqual(stream.readline(3), 'abc') + stream = LimitedStream(StringIO(b'1234\nabcdef'), 9) + self.assertEqual(stream.readline(10), b'1234\n') + self.assertEqual(stream.readline(3), b'abc') # Now expire the available characters - self.assertEqual(stream.readline(3), 'd') + self.assertEqual(stream.readline(3), b'd') # Reading again returns nothing. - self.assertEqual(stream.readline(2), '') + self.assertEqual(stream.readline(2), b'') # Same test, but with read, not readline. - stream = LimitedStream(StringIO('1234\nabcdef'), 9) - self.assertEqual(stream.read(6), '1234\na') - self.assertEqual(stream.read(2), 'bc') - self.assertEqual(stream.read(2), 'd') - self.assertEqual(stream.read(2), '') - self.assertEqual(stream.read(), '') + stream = LimitedStream(StringIO(b'1234\nabcdef'), 9) + self.assertEqual(stream.read(6), b'1234\na') + self.assertEqual(stream.read(2), b'bc') + self.assertEqual(stream.read(2), b'd') + self.assertEqual(stream.read(2), b'') + self.assertEqual(stream.read(), b'') def test_stream(self): - payload = 'name=value' + payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), 'wsgi.input': StringIO(payload)}) - self.assertEqual(request.read(), 'name=value') + self.assertEqual(request.read(), b'name=value') def test_read_after_value(self): """ Reading from request is allowed after accessing request contents as POST or body. """ - payload = 'name=value' + payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), 'wsgi.input': StringIO(payload)}) self.assertEqual(request.POST, {u'name': [u'value']}) - self.assertEqual(request.body, 'name=value') - self.assertEqual(request.read(), 'name=value') + self.assertEqual(request.body, b'name=value') + self.assertEqual(request.read(), b'name=value') def test_value_after_read(self): """ Construction of POST or body is not allowed after reading from request. """ - payload = 'name=value' + payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), 'wsgi.input': StringIO(payload)}) - self.assertEqual(request.read(2), 'na') + self.assertEqual(request.read(2), b'na') self.assertRaises(Exception, lambda: request.body) self.assertEqual(request.POST, {}) @@ -335,17 +335,17 @@ class RequestsTests(unittest.TestCase): self.assertEqual(request.POST, {}) def test_read_by_lines(self): - payload = 'name=value' + payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), 'wsgi.input': StringIO(payload)}) - self.assertEqual(list(request), ['name=value']) + self.assertEqual(list(request), [b'name=value']) def test_POST_after_body_read(self): """ POST should be populated even if body is read first """ - payload = 'name=value' + payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), 'wsgi.input': StringIO(payload)}) @@ -357,12 +357,12 @@ class RequestsTests(unittest.TestCase): POST should be populated even if body is read first, and then the stream is read second. """ - payload = 'name=value' + payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), 'wsgi.input': StringIO(payload)}) raw_data = request.body - self.assertEqual(request.read(1), u'n') + self.assertEqual(request.read(1), b'n') self.assertEqual(request.POST, {u'name': [u'value']}) def test_POST_after_body_read_and_stream_read_multipart(self): @@ -383,14 +383,14 @@ class RequestsTests(unittest.TestCase): 'wsgi.input': StringIO(payload)}) raw_data = request.body # Consume enough data to mess up the parsing: - self.assertEqual(request.read(13), u'--boundary\r\nC') + self.assertEqual(request.read(13), b'--boundary\r\nC') self.assertEqual(request.POST, {u'name': [u'value']}) def test_raw_post_data_returns_body(self): """ HttpRequest.raw_post_body should be the same as HttpRequest.body """ - payload = 'Hello There!' + payload = b'Hello There!' request = WSGIRequest({ 'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), @@ -409,7 +409,7 @@ class RequestsTests(unittest.TestCase): def read(self, len=0): raise IOError("kaboom!") - payload = 'name=value' + payload = b'name=value' request = WSGIRequest({'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH': len(payload), 'wsgi.input': ExplodingStringIO(payload)}) diff --git a/tests/regressiontests/servers/tests.py b/tests/regressiontests/servers/tests.py index 5d287a195b..9537e1feb3 100644 --- a/tests/regressiontests/servers/tests.py +++ b/tests/regressiontests/servers/tests.py @@ -113,7 +113,7 @@ class LiveServerViews(LiveServerBase): Refs #2879. """ f = self.urlopen('/example_view/') - self.assertEqual(f.read(), 'example view') + self.assertEqual(f.read(), b'example view') def test_static_files(self): """ @@ -121,7 +121,7 @@ class LiveServerViews(LiveServerBase): Refs #2879. """ f = self.urlopen('/static/example_static_file.txt') - self.assertEqual(f.read(), 'example static file\n') + self.assertEqual(f.read().rstrip(b'\r\n'), b'example static file') def test_media_files(self): """ @@ -129,7 +129,7 @@ class LiveServerViews(LiveServerBase): Refs #2879. """ f = self.urlopen('/media/example_media_file.txt') - self.assertEqual(f.read(), 'example media file\n') + self.assertEqual(f.read().rstrip(b'\r\n'), b'example media file') class LiveServerDatabase(LiveServerBase): diff --git a/tests/regressiontests/signing/tests.py b/tests/regressiontests/signing/tests.py index 54bc302454..1da3345806 100644 --- a/tests/regressiontests/signing/tests.py +++ b/tests/regressiontests/signing/tests.py @@ -12,8 +12,8 @@ class TestSigner(TestCase): signer = signing.Signer('predictable-secret') signer2 = signing.Signer('predictable-secret2') for s in ( - 'hello', - '3098247:529:087:', + b'hello', + b'3098247:529:087:', u'\u2019'.encode('utf-8'), ): self.assertEqual( @@ -69,7 +69,7 @@ class TestSigner(TestCase): "dumps and loads be reversible for any JSON serializable object" objects = ( ['a', 'list'], - 'a string', + b'a string', u'a unicode string \u2019', {'a': 'dictionary'}, ) diff --git a/tests/regressiontests/staticfiles_tests/storage.py b/tests/regressiontests/staticfiles_tests/storage.py index 8358f833c8..4d49e6fbb2 100644 --- a/tests/regressiontests/staticfiles_tests/storage.py +++ b/tests/regressiontests/staticfiles_tests/storage.py @@ -1,5 +1,6 @@ from datetime import datetime from django.core.files import storage +from django.contrib.staticfiles.storage import CachedStaticFilesStorage class DummyStorage(storage.Storage): """ @@ -17,3 +18,9 @@ class DummyStorage(storage.Storage): def modified_time(self, name): return datetime.date(1970, 1, 1) + + +class SimpleCachedStaticFilesStorage(CachedStaticFilesStorage): + + def file_hash(self, name, content=None): + return 'deploy12345' diff --git a/tests/regressiontests/staticfiles_tests/tests.py b/tests/regressiontests/staticfiles_tests/tests.py index 7f30cb987a..3711d92448 100644 --- a/tests/regressiontests/staticfiles_tests/tests.py +++ b/tests/regressiontests/staticfiles_tests/tests.py @@ -6,11 +6,11 @@ import posixpath import shutil import sys import tempfile -from StringIO import StringIO +from io import BytesIO from django.template import loader, Context from django.conf import settings -from django.core.cache.backends.base import BaseCache, CacheKeyWarning +from django.core.cache.backends.base import BaseCache from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import default_storage from django.core.management import call_command @@ -187,30 +187,22 @@ class TestFindStatic(CollectionTestCase, TestDefaults): Test ``findstatic`` management command. """ def _get_file(self, filepath): - _stdout = sys.stdout - sys.stdout = StringIO() - try: - call_command('findstatic', filepath, all=False, verbosity='0') - sys.stdout.seek(0) - lines = [l.strip() for l in sys.stdout.readlines()] - contents = codecs.open( - smart_unicode(lines[1].strip()), "r", "utf-8").read() - finally: - sys.stdout = _stdout + out = BytesIO() + call_command('findstatic', filepath, all=False, verbosity=0, stdout=out) + out.seek(0) + lines = [l.strip() for l in out.readlines()] + contents = codecs.open( + smart_unicode(lines[1].strip()), "r", "utf-8").read() return contents def test_all_files(self): """ Test that findstatic returns all candidate files if run without --first. """ - _stdout = sys.stdout - sys.stdout = StringIO() - try: - call_command('findstatic', 'test/file.txt', verbosity='0') - sys.stdout.seek(0) - lines = [l.strip() for l in sys.stdout.readlines()] - finally: - sys.stdout = _stdout + out = BytesIO() + call_command('findstatic', 'test/file.txt', verbosity=0, stdout=out) + out.seek(0) + lines = [l.strip() for l in out.readlines()] self.assertEqual(len(lines), 3) # three because there is also the "Found here" line self.assertIn('project', lines[1]) self.assertIn('apps', lines[2]) @@ -387,8 +379,8 @@ class TestCollectionCachedStorage(BaseCollectionTestCase, self.assertEqual(relpath, "cached/styles.93b1147e8552.css") with storage.staticfiles_storage.open(relpath) as relfile: content = relfile.read() - self.assertNotIn("cached/other.css", content) - self.assertIn("other.d41d8cd98f00.css", content) + self.assertNotIn(b"cached/other.css", content) + self.assertIn(b"other.d41d8cd98f00.css", content) def test_path_with_querystring(self): relpath = self.cached_file_path("cached/styles.css?spam=eggs") @@ -397,8 +389,8 @@ class TestCollectionCachedStorage(BaseCollectionTestCase, with storage.staticfiles_storage.open( "cached/styles.93b1147e8552.css") as relfile: content = relfile.read() - self.assertNotIn("cached/other.css", content) - self.assertIn("other.d41d8cd98f00.css", content) + self.assertNotIn(b"cached/other.css", content) + self.assertIn(b"other.d41d8cd98f00.css", content) def test_path_with_fragment(self): relpath = self.cached_file_path("cached/styles.css#eggs") @@ -406,62 +398,62 @@ class TestCollectionCachedStorage(BaseCollectionTestCase, with storage.staticfiles_storage.open( "cached/styles.93b1147e8552.css") as relfile: content = relfile.read() - self.assertNotIn("cached/other.css", content) - self.assertIn("other.d41d8cd98f00.css", content) + self.assertNotIn(b"cached/other.css", content) + self.assertIn(b"other.d41d8cd98f00.css", content) def test_path_with_querystring_and_fragment(self): relpath = self.cached_file_path("cached/css/fragments.css") self.assertEqual(relpath, "cached/css/fragments.75433540b096.css") with storage.staticfiles_storage.open(relpath) as relfile: content = relfile.read() - self.assertIn('fonts/font.a4b0478549d0.eot?#iefix', content) - self.assertIn('fonts/font.b8d603e42714.svg#webfontIyfZbseF', content) - self.assertIn('data:font/woff;charset=utf-8;base64,d09GRgABAAAAADJoAA0AAAAAR2QAAQAAAAAAAAAAAAA', content) - self.assertIn('#default#VML', content) + self.assertIn(b'fonts/font.a4b0478549d0.eot?#iefix', content) + self.assertIn(b'fonts/font.b8d603e42714.svg#webfontIyfZbseF', content) + self.assertIn(b'data:font/woff;charset=utf-8;base64,d09GRgABAAAAADJoAA0AAAAAR2QAAQAAAAAAAAAAAAA', content) + self.assertIn(b'#default#VML', content) def test_template_tag_absolute(self): relpath = self.cached_file_path("cached/absolute.css") self.assertEqual(relpath, "cached/absolute.23f087ad823a.css") with storage.staticfiles_storage.open(relpath) as relfile: content = relfile.read() - self.assertNotIn("/static/cached/styles.css", content) - self.assertIn("/static/cached/styles.93b1147e8552.css", content) - self.assertIn('/static/cached/img/relative.acae32e4532b.png', content) + self.assertNotIn(b"/static/cached/styles.css", content) + self.assertIn(b"/static/cached/styles.93b1147e8552.css", content) + self.assertIn(b'/static/cached/img/relative.acae32e4532b.png', content) def test_template_tag_denorm(self): relpath = self.cached_file_path("cached/denorm.css") self.assertEqual(relpath, "cached/denorm.c5bd139ad821.css") with storage.staticfiles_storage.open(relpath) as relfile: content = relfile.read() - self.assertNotIn("..//cached///styles.css", content) - self.assertIn("../cached/styles.93b1147e8552.css", content) - self.assertNotIn("url(img/relative.png )", content) - self.assertIn('url("img/relative.acae32e4532b.png', content) + self.assertNotIn(b"..//cached///styles.css", content) + self.assertIn(b"../cached/styles.93b1147e8552.css", content) + self.assertNotIn(b"url(img/relative.png )", content) + self.assertIn(b'url("img/relative.acae32e4532b.png', content) def test_template_tag_relative(self): relpath = self.cached_file_path("cached/relative.css") self.assertEqual(relpath, "cached/relative.2217ea7273c2.css") with storage.staticfiles_storage.open(relpath) as relfile: content = relfile.read() - self.assertNotIn("../cached/styles.css", content) - self.assertNotIn('@import "styles.css"', content) - self.assertNotIn('url(img/relative.png)', content) - self.assertIn('url("img/relative.acae32e4532b.png")', content) - self.assertIn("../cached/styles.93b1147e8552.css", content) + self.assertNotIn(b"../cached/styles.css", content) + self.assertNotIn(b'@import "styles.css"', content) + self.assertNotIn(b'url(img/relative.png)', content) + self.assertIn(b'url("img/relative.acae32e4532b.png")', content) + self.assertIn(b"../cached/styles.93b1147e8552.css", content) def test_template_tag_deep_relative(self): relpath = self.cached_file_path("cached/css/window.css") self.assertEqual(relpath, "cached/css/window.9db38d5169f3.css") with storage.staticfiles_storage.open(relpath) as relfile: content = relfile.read() - self.assertNotIn('url(img/window.png)', content) - self.assertIn('url("img/window.acae32e4532b.png")', content) + self.assertNotIn(b'url(img/window.png)', content) + self.assertIn(b'url("img/window.acae32e4532b.png")', content) def test_template_tag_url(self): relpath = self.cached_file_path("cached/url.css") self.assertEqual(relpath, "cached/url.615e21601e4b.css") with storage.staticfiles_storage.open(relpath) as relfile: - self.assertIn("https://", relfile.read()) + self.assertIn(b"https://", relfile.read()) def test_cache_invalidation(self): name = "cached/styles.css" @@ -508,13 +500,51 @@ class TestCollectionCachedStorage(BaseCollectionTestCase, """ Handle cache key creation correctly, see #17861. """ - name = "/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/" + chr(22) + chr(180) + name = b"/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/long filename/ with spaces Here and ?#%#$/other/stuff/some crazy/" + chr(22) + chr(180) cache_key = storage.staticfiles_storage.cache_key(name) cache_validator = BaseCache({}) cache_validator.validate_key(cache_key) self.assertEqual(cache_key, 'staticfiles:e95bbc36387084582df2a70750d7b351') +# we set DEBUG to False here since the template tag wouldn't work otherwise +@override_settings(**dict(TEST_SETTINGS, + STATICFILES_STORAGE='regressiontests.staticfiles_tests.storage.SimpleCachedStaticFilesStorage', + DEBUG=False, +)) +class TestCollectionSimpleCachedStorage(BaseCollectionTestCase, + BaseStaticFilesTestCase, TestCase): + """ + Tests for the Cache busting storage + """ + def cached_file_path(self, path): + fullpath = self.render_template(self.static_template_snippet(path)) + return fullpath.replace(settings.STATIC_URL, '') + + def test_template_tag_return(self): + """ + Test the CachedStaticFilesStorage backend. + """ + self.assertStaticRaises(ValueError, + "does/not/exist.png", + "/static/does/not/exist.png") + self.assertStaticRenders("test/file.txt", + "/static/test/file.deploy12345.txt") + self.assertStaticRenders("cached/styles.css", + "/static/cached/styles.deploy12345.css") + self.assertStaticRenders("path/", + "/static/path/") + self.assertStaticRenders("path/?query", + "/static/path/?query") + + def test_template_tag_simple_content(self): + relpath = self.cached_file_path("cached/styles.css") + self.assertEqual(relpath, "cached/styles.deploy12345.css") + with storage.staticfiles_storage.open(relpath) as relfile: + content = relfile.read() + self.assertNotIn("cached/other.css", content) + self.assertIn("other.deploy12345.css", content) + if sys.platform != 'win32': class TestCollectionLinks(CollectionTestCase, TestDefaults): diff --git a/tests/regressiontests/string_lookup/tests.py b/tests/regressiontests/string_lookup/tests.py index f5ae73a6ca..b3f90169ca 100644 --- a/tests/regressiontests/string_lookup/tests.py +++ b/tests/regressiontests/string_lookup/tests.py @@ -54,7 +54,7 @@ class StringLookupTests(TestCase): self.assertEqual(Foo.objects.get(friend__contains=u'\xe7'), fx) # We can also do the above query using UTF-8 strings. - self.assertEqual(Foo.objects.get(friend__contains='\xc3\xa7'), fx) + self.assertEqual(Foo.objects.get(friend__contains=b'\xc3\xa7'), fx) def test_queries_on_textfields(self): """ diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index d4ef2aa574..8590dd25af 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -192,17 +192,17 @@ class Templates(unittest.TestCase): test_template_sources('../dir1blah', template_dirs, []) # UTF-8 bytestrings are permitted. - test_template_sources('\xc3\x85ngstr\xc3\xb6m', template_dirs, + test_template_sources(b'\xc3\x85ngstr\xc3\xb6m', template_dirs, [u'/dir1/Ångström', u'/dir2/Ångström']) # Unicode strings are permitted. test_template_sources(u'Ångström', template_dirs, [u'/dir1/Ångström', u'/dir2/Ångström']) - test_template_sources(u'Ångström', ['/Straße'], [u'/Straße/Ångström']) - test_template_sources('\xc3\x85ngstr\xc3\xb6m', ['/Straße'], + test_template_sources(u'Ångström', [b'/Stra\xc3\x9fe'], [u'/Straße/Ångström']) + test_template_sources(b'\xc3\x85ngstr\xc3\xb6m', [b'/Stra\xc3\x9fe'], [u'/Straße/Ångström']) # Invalid UTF-8 encoding in bytestrings is not. Should raise a # semi-useful error message. - test_template_sources('\xc3\xc3', template_dirs, UnicodeDecodeError) + test_template_sources(b'\xc3\xc3', template_dirs, UnicodeDecodeError) # Case insensitive tests (for win32). Not run unless we're on # a case insensitive operating system. @@ -1239,11 +1239,11 @@ class Templates(unittest.TestCase): 'i18n02': ('{% load i18n %}{% trans "xxxyyyxxx" %}', {}, "xxxyyyxxx"), # simple translation of a variable - 'i18n03': ('{% load i18n %}{% blocktrans %}{{ anton }}{% endblocktrans %}', {'anton': '\xc3\x85'}, u"Å"), + 'i18n03': ('{% load i18n %}{% blocktrans %}{{ anton }}{% endblocktrans %}', {'anton': b'\xc3\x85'}, u"Å"), # simple translation of a variable and filter - 'i18n04': ('{% load i18n %}{% blocktrans with berta=anton|lower %}{{ berta }}{% endblocktrans %}', {'anton': '\xc3\x85'}, u'å'), - 'legacyi18n04': ('{% load i18n %}{% blocktrans with anton|lower as berta %}{{ berta }}{% endblocktrans %}', {'anton': '\xc3\x85'}, u'å'), + 'i18n04': ('{% load i18n %}{% blocktrans with berta=anton|lower %}{{ berta }}{% endblocktrans %}', {'anton': b'\xc3\x85'}, u'å'), + 'legacyi18n04': ('{% load i18n %}{% blocktrans with anton|lower as berta %}{{ berta }}{% endblocktrans %}', {'anton': b'\xc3\x85'}, u'å'), # simple translation of a string with interpolation 'i18n05': ('{% load i18n %}{% blocktrans %}xxx{{ anton }}xxx{% endblocktrans %}', {'anton': 'yyy'}, "xxxyyyxxx"), diff --git a/tests/regressiontests/templates/unicode.py b/tests/regressiontests/templates/unicode.py index c8d7309ad7..0e0cefeb8e 100644 --- a/tests/regressiontests/templates/unicode.py +++ b/tests/regressiontests/templates/unicode.py @@ -10,16 +10,16 @@ class UnicodeTests(TestCase): t1 = Template(u'ŠĐĆŽćžšđ {{ var }}') # Templates can also be created from bytestrings. These are assumed to # be encoded using UTF-8. - s = '\xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91 {{ var }}' + s = b'\xc5\xa0\xc4\x90\xc4\x86\xc5\xbd\xc4\x87\xc5\xbe\xc5\xa1\xc4\x91 {{ var }}' t2 = Template(s) - s = '\x80\xc5\xc0' + s = b'\x80\xc5\xc0' self.assertRaises(TemplateEncodingError, Template, s) # Contexts can be constructed from unicode or UTF-8 bytestrings. - c1 = Context({"var": "foo"}) - c2 = Context({u"var": "foo"}) - c3 = Context({"var": u"Đđ"}) - c4 = Context({u"var": "\xc4\x90\xc4\x91"}) + c1 = Context({b"var": b"foo"}) + c2 = Context({u"var": b"foo"}) + c3 = Context({b"var": u"Đđ"}) + c4 = Context({u"var": b"\xc4\x90\xc4\x91"}) # Since both templates and all four contexts represent the same thing, # they all render the same (and are returned as unicode objects and diff --git a/tests/regressiontests/test_client_regress/models.py b/tests/regressiontests/test_client_regress/models.py index 4f057b934c..e69de29bb2 100644 --- a/tests/regressiontests/test_client_regress/models.py +++ b/tests/regressiontests/test_client_regress/models.py @@ -1,1034 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Regression tests for the Test Client, especially the customized assertions. -""" -import os - -from django.conf import settings -from django.core.exceptions import SuspiciousOperation -from django.core.urlresolvers import reverse -from django.template import (TemplateDoesNotExist, TemplateSyntaxError, - Context, Template, loader) -import django.template.context -from django.test import Client, TestCase -from django.test.client import encode_file, RequestFactory -from django.test.utils import ContextList, override_settings -from django.template.response import SimpleTemplateResponse -from django.http import HttpResponse - - -class AssertContainsTests(TestCase): - def setUp(self): - self.old_templates = settings.TEMPLATE_DIRS - settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), 'templates'),) - - def tearDown(self): - settings.TEMPLATE_DIRS = self.old_templates - - def test_contains(self): - "Responses can be inspected for content, including counting repeated substrings" - response = self.client.get('/test_client_regress/no_template_view/') - - self.assertNotContains(response, 'never') - self.assertContains(response, 'never', 0) - self.assertContains(response, 'once') - self.assertContains(response, 'once', 1) - self.assertContains(response, 'twice') - self.assertContains(response, 'twice', 2) - - try: - self.assertContains(response, 'text', status_code=999) - except AssertionError as e: - self.assertIn("Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) - try: - self.assertContains(response, 'text', status_code=999, msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) - - try: - self.assertNotContains(response, 'text', status_code=999) - except AssertionError as e: - self.assertIn("Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) - try: - self.assertNotContains(response, 'text', status_code=999, msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) - - try: - self.assertNotContains(response, 'once') - except AssertionError as e: - self.assertIn("Response should not contain 'once'", str(e)) - try: - self.assertNotContains(response, 'once', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Response should not contain 'once'", str(e)) - - try: - self.assertContains(response, 'never', 1) - except AssertionError as e: - self.assertIn("Found 0 instances of 'never' in response (expected 1)", str(e)) - try: - self.assertContains(response, 'never', 1, msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Found 0 instances of 'never' in response (expected 1)", str(e)) - - try: - self.assertContains(response, 'once', 0) - except AssertionError as e: - self.assertIn("Found 1 instances of 'once' in response (expected 0)", str(e)) - try: - self.assertContains(response, 'once', 0, msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Found 1 instances of 'once' in response (expected 0)", str(e)) - - try: - self.assertContains(response, 'once', 2) - except AssertionError as e: - self.assertIn("Found 1 instances of 'once' in response (expected 2)", str(e)) - try: - self.assertContains(response, 'once', 2, msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Found 1 instances of 'once' in response (expected 2)", str(e)) - - try: - self.assertContains(response, 'twice', 1) - except AssertionError as e: - self.assertIn("Found 2 instances of 'twice' in response (expected 1)", str(e)) - try: - self.assertContains(response, 'twice', 1, msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Found 2 instances of 'twice' in response (expected 1)", str(e)) - - try: - self.assertContains(response, 'thrice') - except AssertionError as e: - self.assertIn("Couldn't find 'thrice' in response", str(e)) - try: - self.assertContains(response, 'thrice', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Couldn't find 'thrice' in response", str(e)) - - try: - self.assertContains(response, 'thrice', 3) - except AssertionError as e: - self.assertIn("Found 0 instances of 'thrice' in response (expected 3)", str(e)) - try: - self.assertContains(response, 'thrice', 3, msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Found 0 instances of 'thrice' in response (expected 3)", str(e)) - - def test_unicode_contains(self): - "Unicode characters can be found in template context" - #Regression test for #10183 - r = self.client.get('/test_client_regress/check_unicode/') - self.assertContains(r, u'さかき') - self.assertContains(r, '\xe5\xb3\xa0'.decode('utf-8')) - - def test_unicode_not_contains(self): - "Unicode characters can be searched for, and not found in template context" - #Regression test for #10183 - r = self.client.get('/test_client_regress/check_unicode/') - self.assertNotContains(r, u'はたけ') - self.assertNotContains(r, '\xe3\x81\xaf\xe3\x81\x9f\xe3\x81\x91'.decode('utf-8')) - - def test_assert_contains_renders_template_response(self): - """ Test that we can pass in an unrendered SimpleTemplateReponse - without throwing an error. - Refs #15826. - """ - response = SimpleTemplateResponse(Template('Hello'), status=200) - self.assertContains(response, 'Hello') - - def test_assert_contains_using_non_template_response(self): - """ Test that auto-rendering does not affect responses that aren't - instances (or subclasses) of SimpleTemplateResponse. - Refs #15826. - """ - response = HttpResponse('Hello') - self.assertContains(response, 'Hello') - - def test_assert_not_contains_renders_template_response(self): - """ Test that we can pass in an unrendered SimpleTemplateReponse - without throwing an error. - Refs #15826. - """ - response = SimpleTemplateResponse(Template('Hello'), status=200) - self.assertNotContains(response, 'Bye') - - def test_assert_not_contains_using_non_template_response(self): - """ Test that auto-rendering does not affect responses that aren't - instances (or subclasses) of SimpleTemplateResponse. - Refs #15826. - """ - response = HttpResponse('Hello') - self.assertNotContains(response, 'Bye') - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class AssertTemplateUsedTests(TestCase): - fixtures = ['testdata.json'] - - def test_no_context(self): - "Template usage assertions work then templates aren't in use" - response = self.client.get('/test_client_regress/no_template_view/') - - # Check that the no template case doesn't mess with the template assertions - self.assertTemplateNotUsed(response, 'GET Template') - - try: - self.assertTemplateUsed(response, 'GET Template') - except AssertionError as e: - self.assertIn("No templates used to render the response", str(e)) - - try: - self.assertTemplateUsed(response, 'GET Template', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: No templates used to render the response", str(e)) - - def test_single_context(self): - "Template assertions work when there is a single context" - response = self.client.get('/test_client/post_view/', {}) - - try: - self.assertTemplateNotUsed(response, 'Empty GET Template') - except AssertionError as e: - self.assertIn("Template 'Empty GET Template' was used unexpectedly in rendering the response", str(e)) - - try: - self.assertTemplateNotUsed(response, 'Empty GET Template', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Template 'Empty GET Template' was used unexpectedly in rendering the response", str(e)) - - try: - self.assertTemplateUsed(response, 'Empty POST Template') - except AssertionError as e: - self.assertIn("Template 'Empty POST Template' was not a template used to render the response. Actual template(s) used: Empty GET Template", str(e)) - - try: - self.assertTemplateUsed(response, 'Empty POST Template', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Template 'Empty POST Template' was not a template used to render the response. Actual template(s) used: Empty GET Template", str(e)) - - def test_multiple_context(self): - "Template assertions work when there are multiple contexts" - post_data = { - 'text': 'Hello World', - 'email': 'foo@example.com', - 'value': 37, - 'single': 'b', - 'multi': ('b','c','e') - } - response = self.client.post('/test_client/form_view_with_template/', post_data) - self.assertContains(response, 'POST data OK') - try: - self.assertTemplateNotUsed(response, "form_view.html") - except AssertionError as e: - self.assertIn("Template 'form_view.html' was used unexpectedly in rendering the response", str(e)) - - try: - self.assertTemplateNotUsed(response, 'base.html') - except AssertionError as e: - self.assertIn("Template 'base.html' was used unexpectedly in rendering the response", str(e)) - - try: - self.assertTemplateUsed(response, "Valid POST Template") - except AssertionError as e: - self.assertIn("Template 'Valid POST Template' was not a template used to render the response. Actual template(s) used: form_view.html, base.html", str(e)) - -class AssertRedirectsTests(TestCase): - def test_redirect_page(self): - "An assertion is raised if the original page couldn't be retrieved as expected" - # This page will redirect with code 301, not 302 - response = self.client.get('/test_client/permanent_redirect_view/') - try: - self.assertRedirects(response, '/test_client/get_view/') - except AssertionError as e: - self.assertIn("Response didn't redirect as expected: Response code was 301 (expected 302)", str(e)) - - try: - self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Response didn't redirect as expected: Response code was 301 (expected 302)", str(e)) - - def test_lost_query(self): - "An assertion is raised if the redirect location doesn't preserve GET parameters" - response = self.client.get('/test_client/redirect_view/', {'var': 'value'}) - try: - self.assertRedirects(response, '/test_client/get_view/') - except AssertionError as e: - self.assertIn("Response redirected to 'http://testserver/test_client/get_view/?var=value', expected 'http://testserver/test_client/get_view/'", str(e)) - - try: - self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Response redirected to 'http://testserver/test_client/get_view/?var=value', expected 'http://testserver/test_client/get_view/'", str(e)) - - def test_incorrect_target(self): - "An assertion is raised if the response redirects to another target" - response = self.client.get('/test_client/permanent_redirect_view/') - try: - # Should redirect to get_view - self.assertRedirects(response, '/test_client/some_view/') - except AssertionError as e: - self.assertIn("Response didn't redirect as expected: Response code was 301 (expected 302)", str(e)) - - def test_target_page(self): - "An assertion is raised if the response redirect target cannot be retrieved as expected" - response = self.client.get('/test_client/double_redirect_view/') - try: - # The redirect target responds with a 301 code, not 200 - self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/') - except AssertionError as e: - self.assertIn("Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)", str(e)) - - try: - # The redirect target responds with a 301 code, not 200 - self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)", str(e)) - - def test_redirect_chain(self): - "You can follow a redirect chain of multiple redirects" - response = self.client.get('/test_client_regress/redirects/further/more/', {}, follow=True) - self.assertRedirects(response, '/test_client_regress/no_template_view/', - status_code=301, target_status_code=200) - - self.assertEqual(len(response.redirect_chain), 1) - self.assertEqual(response.redirect_chain[0], ('http://testserver/test_client_regress/no_template_view/', 301)) - - def test_multiple_redirect_chain(self): - "You can follow a redirect chain of multiple redirects" - response = self.client.get('/test_client_regress/redirects/', {}, follow=True) - self.assertRedirects(response, '/test_client_regress/no_template_view/', - status_code=301, target_status_code=200) - - self.assertEqual(len(response.redirect_chain), 3) - self.assertEqual(response.redirect_chain[0], ('http://testserver/test_client_regress/redirects/further/', 301)) - self.assertEqual(response.redirect_chain[1], ('http://testserver/test_client_regress/redirects/further/more/', 301)) - self.assertEqual(response.redirect_chain[2], ('http://testserver/test_client_regress/no_template_view/', 301)) - - def test_redirect_chain_to_non_existent(self): - "You can follow a chain to a non-existent view" - response = self.client.get('/test_client_regress/redirect_to_non_existent_view2/', {}, follow=True) - self.assertRedirects(response, '/test_client_regress/non_existent_view/', - status_code=301, target_status_code=404) - - def test_redirect_chain_to_self(self): - "Redirections to self are caught and escaped" - response = self.client.get('/test_client_regress/redirect_to_self/', {}, follow=True) - # The chain of redirects stops once the cycle is detected. - self.assertRedirects(response, '/test_client_regress/redirect_to_self/', - status_code=301, target_status_code=301) - self.assertEqual(len(response.redirect_chain), 2) - - def test_circular_redirect(self): - "Circular redirect chains are caught and escaped" - response = self.client.get('/test_client_regress/circular_redirect_1/', {}, follow=True) - # The chain of redirects will get back to the starting point, but stop there. - self.assertRedirects(response, '/test_client_regress/circular_redirect_2/', - status_code=301, target_status_code=301) - self.assertEqual(len(response.redirect_chain), 4) - - def test_redirect_chain_post(self): - "A redirect chain will be followed from an initial POST post" - response = self.client.post('/test_client_regress/redirects/', - {'nothing': 'to_send'}, follow=True) - self.assertRedirects(response, - '/test_client_regress/no_template_view/', 301, 200) - self.assertEqual(len(response.redirect_chain), 3) - - def test_redirect_chain_head(self): - "A redirect chain will be followed from an initial HEAD request" - response = self.client.head('/test_client_regress/redirects/', - {'nothing': 'to_send'}, follow=True) - self.assertRedirects(response, - '/test_client_regress/no_template_view/', 301, 200) - self.assertEqual(len(response.redirect_chain), 3) - - def test_redirect_chain_options(self): - "A redirect chain will be followed from an initial OPTIONS request" - response = self.client.options('/test_client_regress/redirects/', - {'nothing': 'to_send'}, follow=True) - self.assertRedirects(response, - '/test_client_regress/no_template_view/', 301, 200) - self.assertEqual(len(response.redirect_chain), 3) - - def test_redirect_chain_put(self): - "A redirect chain will be followed from an initial PUT request" - response = self.client.put('/test_client_regress/redirects/', - {'nothing': 'to_send'}, follow=True) - self.assertRedirects(response, - '/test_client_regress/no_template_view/', 301, 200) - self.assertEqual(len(response.redirect_chain), 3) - - def test_redirect_chain_delete(self): - "A redirect chain will be followed from an initial DELETE request" - response = self.client.delete('/test_client_regress/redirects/', - {'nothing': 'to_send'}, follow=True) - self.assertRedirects(response, - '/test_client_regress/no_template_view/', 301, 200) - self.assertEqual(len(response.redirect_chain), 3) - - def test_redirect_to_different_host(self): - "The test client will preserve scheme, host and port changes" - response = self.client.get('/test_client_regress/redirect_other_host/', follow=True) - self.assertRedirects(response, - 'https://otherserver:8443/test_client_regress/no_template_view/', - status_code=301, target_status_code=200) - # We can't use is_secure() or get_host() - # because response.request is a dictionary, not an HttpRequest - self.assertEqual(response.request.get('wsgi.url_scheme'), 'https') - self.assertEqual(response.request.get('SERVER_NAME'), 'otherserver') - self.assertEqual(response.request.get('SERVER_PORT'), '8443') - - def test_redirect_chain_on_non_redirect_page(self): - "An assertion is raised if the original page couldn't be retrieved as expected" - # This page will redirect with code 301, not 302 - response = self.client.get('/test_client/get_view/', follow=True) - try: - self.assertRedirects(response, '/test_client/get_view/') - except AssertionError as e: - self.assertIn("Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) - - try: - self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) - - def test_redirect_on_non_redirect_page(self): - "An assertion is raised if the original page couldn't be retrieved as expected" - # This page will redirect with code 301, not 302 - response = self.client.get('/test_client/get_view/') - try: - self.assertRedirects(response, '/test_client/get_view/') - except AssertionError as e: - self.assertIn("Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) - - try: - self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) - - -class AssertFormErrorTests(TestCase): - def test_unknown_form(self): - "An assertion is raised if the form name is unknown" - post_data = { - 'text': 'Hello World', - 'email': 'not an email address', - 'value': 37, - 'single': 'b', - 'multi': ('b','c','e') - } - response = self.client.post('/test_client/form_view/', post_data) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "Invalid POST Template") - - try: - self.assertFormError(response, 'wrong_form', 'some_field', 'Some error.') - except AssertionError as e: - self.assertIn("The form 'wrong_form' was not used to render the response", str(e)) - try: - self.assertFormError(response, 'wrong_form', 'some_field', 'Some error.', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: The form 'wrong_form' was not used to render the response", str(e)) - - def test_unknown_field(self): - "An assertion is raised if the field name is unknown" - post_data = { - 'text': 'Hello World', - 'email': 'not an email address', - 'value': 37, - 'single': 'b', - 'multi': ('b','c','e') - } - response = self.client.post('/test_client/form_view/', post_data) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "Invalid POST Template") - - try: - self.assertFormError(response, 'form', 'some_field', 'Some error.') - except AssertionError as e: - self.assertIn("The form 'form' in context 0 does not contain the field 'some_field'", str(e)) - try: - self.assertFormError(response, 'form', 'some_field', 'Some error.', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: The form 'form' in context 0 does not contain the field 'some_field'", str(e)) - - def test_noerror_field(self): - "An assertion is raised if the field doesn't have any errors" - post_data = { - 'text': 'Hello World', - 'email': 'not an email address', - 'value': 37, - 'single': 'b', - 'multi': ('b','c','e') - } - response = self.client.post('/test_client/form_view/', post_data) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "Invalid POST Template") - - try: - self.assertFormError(response, 'form', 'value', 'Some error.') - except AssertionError as e: - self.assertIn("The field 'value' on form 'form' in context 0 contains no errors", str(e)) - try: - self.assertFormError(response, 'form', 'value', 'Some error.', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: The field 'value' on form 'form' in context 0 contains no errors", str(e)) - - def test_unknown_error(self): - "An assertion is raised if the field doesn't contain the provided error" - post_data = { - 'text': 'Hello World', - 'email': 'not an email address', - 'value': 37, - 'single': 'b', - 'multi': ('b','c','e') - } - response = self.client.post('/test_client/form_view/', post_data) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "Invalid POST Template") - - try: - self.assertFormError(response, 'form', 'email', 'Some error.') - except AssertionError as e: - self.assertIn("The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [u'Enter a valid e-mail address.'])", str(e)) - try: - self.assertFormError(response, 'form', 'email', 'Some error.', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [u'Enter a valid e-mail address.'])", str(e)) - - def test_unknown_nonfield_error(self): - """ - Checks that an assertion is raised if the form's non field errors - doesn't contain the provided error. - """ - post_data = { - 'text': 'Hello World', - 'email': 'not an email address', - 'value': 37, - 'single': 'b', - 'multi': ('b','c','e') - } - response = self.client.post('/test_client/form_view/', post_data) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "Invalid POST Template") - - try: - self.assertFormError(response, 'form', None, 'Some error.') - except AssertionError as e: - self.assertIn("The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e)) - try: - self.assertFormError(response, 'form', None, 'Some error.', msg_prefix='abc') - except AssertionError as e: - self.assertIn("abc: The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e)) - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class LoginTests(TestCase): - fixtures = ['testdata'] - - def test_login_different_client(self): - "Check that using a different test client doesn't violate authentication" - - # Create a second client, and log in. - c = Client() - login = c.login(username='testclient', password='password') - self.assertTrue(login, 'Could not log in') - - # Get a redirection page with the second client. - response = c.get("/test_client_regress/login_protected_redirect_view/") - - # At this points, the self.client isn't logged in. - # Check that assertRedirects uses the original client, not the - # default client. - self.assertRedirects(response, "http://testserver/test_client_regress/get_view/") - - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class SessionEngineTests(TestCase): - fixtures = ['testdata'] - - def setUp(self): - self.old_SESSION_ENGINE = settings.SESSION_ENGINE - settings.SESSION_ENGINE = 'regressiontests.test_client_regress.session' - - def tearDown(self): - settings.SESSION_ENGINE = self.old_SESSION_ENGINE - - def test_login(self): - "A session engine that modifies the session key can be used to log in" - login = self.client.login(username='testclient', password='password') - self.assertTrue(login, 'Could not log in') - - # Try to access a login protected page. - response = self.client.get("/test_client/login_protected_view/") - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['user'].username, 'testclient') - - -class URLEscapingTests(TestCase): - def test_simple_argument_get(self): - "Get a view that has a simple string argument" - response = self.client.get(reverse('arg_view', args=['Slartibartfast'])) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'Howdy, Slartibartfast') - - def test_argument_with_space_get(self): - "Get a view that has a string argument that requires escaping" - response = self.client.get(reverse('arg_view', args=['Arthur Dent'])) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'Hi, Arthur') - - def test_simple_argument_post(self): - "Post for a view that has a simple string argument" - response = self.client.post(reverse('arg_view', args=['Slartibartfast'])) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'Howdy, Slartibartfast') - - def test_argument_with_space_post(self): - "Post for a view that has a string argument that requires escaping" - response = self.client.post(reverse('arg_view', args=['Arthur Dent'])) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'Hi, Arthur') - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class ExceptionTests(TestCase): - fixtures = ['testdata.json'] - - def test_exception_cleared(self): - "#5836 - A stale user exception isn't re-raised by the test client." - - login = self.client.login(username='testclient',password='password') - self.assertTrue(login, 'Could not log in') - try: - response = self.client.get("/test_client_regress/staff_only/") - self.fail("General users should not be able to visit this page") - except SuspiciousOperation: - pass - - # At this point, an exception has been raised, and should be cleared. - - # This next operation should be successful; if it isn't we have a problem. - login = self.client.login(username='staff', password='password') - self.assertTrue(login, 'Could not log in') - try: - self.client.get("/test_client_regress/staff_only/") - except SuspiciousOperation: - self.fail("Staff should be able to visit this page") - -class TemplateExceptionTests(TestCase): - def setUp(self): - # Reset the loaders so they don't try to render cached templates. - if loader.template_source_loaders is not None: - for template_loader in loader.template_source_loaders: - if hasattr(template_loader, 'reset'): - template_loader.reset() - self.old_templates = settings.TEMPLATE_DIRS - settings.TEMPLATE_DIRS = () - - def tearDown(self): - settings.TEMPLATE_DIRS = self.old_templates - - def test_no_404_template(self): - "Missing templates are correctly reported by test client" - try: - response = self.client.get("/no_such_view/") - self.fail("Should get error about missing template") - except TemplateDoesNotExist: - pass - - def test_bad_404_template(self): - "Errors found when rendering 404 error templates are re-raised" - settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), 'bad_templates'),) - try: - response = self.client.get("/no_such_view/") - self.fail("Should get error about syntax error in template") - except TemplateSyntaxError: - pass - -# We need two different tests to check URLconf substitution - one to check -# it was changed, and another one (without self.urls) to check it was reverted on -# teardown. This pair of tests relies upon the alphabetical ordering of test execution. -class UrlconfSubstitutionTests(TestCase): - urls = 'regressiontests.test_client_regress.urls' - - def test_urlconf_was_changed(self): - "TestCase can enforce a custom URLconf on a per-test basis" - url = reverse('arg_view', args=['somename']) - self.assertEqual(url, '/arg_view/somename/') - -# This test needs to run *after* UrlconfSubstitutionTests; the zz prefix in the -# name is to ensure alphabetical ordering. -class zzUrlconfSubstitutionTests(TestCase): - def test_urlconf_was_reverted(self): - "URLconf is reverted to original value after modification in a TestCase" - url = reverse('arg_view', args=['somename']) - self.assertEqual(url, '/test_client_regress/arg_view/somename/') - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class ContextTests(TestCase): - fixtures = ['testdata'] - - def test_single_context(self): - "Context variables can be retrieved from a single context" - response = self.client.get("/test_client_regress/request_data/", data={'foo':'whiz'}) - self.assertEqual(response.context.__class__, Context) - self.assertTrue('get-foo' in response.context) - self.assertEqual(response.context['get-foo'], 'whiz') - self.assertEqual(response.context['request-foo'], 'whiz') - self.assertEqual(response.context['data'], 'sausage') - - try: - response.context['does-not-exist'] - self.fail('Should not be able to retrieve non-existent key') - except KeyError as e: - self.assertEqual(e.args[0], 'does-not-exist') - - def test_inherited_context(self): - "Context variables can be retrieved from a list of contexts" - response = self.client.get("/test_client_regress/request_data_extended/", data={'foo':'whiz'}) - self.assertEqual(response.context.__class__, ContextList) - self.assertEqual(len(response.context), 2) - self.assertTrue('get-foo' in response.context) - self.assertEqual(response.context['get-foo'], 'whiz') - self.assertEqual(response.context['request-foo'], 'whiz') - self.assertEqual(response.context['data'], 'bacon') - - try: - response.context['does-not-exist'] - self.fail('Should not be able to retrieve non-existent key') - except KeyError as e: - self.assertEqual(e.args[0], 'does-not-exist') - - def test_15368(self): - # Need to insert a context processor that assumes certain things about - # the request instance. This triggers a bug caused by some ways of - # copying RequestContext. - try: - django.template.context._standard_context_processors = (lambda request: {'path': request.special_path},) - response = self.client.get("/test_client_regress/request_context_view/") - self.assertContains(response, 'Path: /test_client_regress/request_context_view/') - finally: - django.template.context._standard_context_processors = None - - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class SessionTests(TestCase): - fixtures = ['testdata.json'] - - def test_session(self): - "The session isn't lost if a user logs in" - # The session doesn't exist to start. - response = self.client.get('/test_client_regress/check_session/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'NO') - - # This request sets a session variable. - response = self.client.get('/test_client_regress/set_session/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'set_session') - - # Check that the session has been modified - response = self.client.get('/test_client_regress/check_session/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'YES') - - # Log in - login = self.client.login(username='testclient',password='password') - self.assertTrue(login, 'Could not log in') - - # Session should still contain the modified value - response = self.client.get('/test_client_regress/check_session/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'YES') - - def test_logout(self): - """Logout should work whether the user is logged in or not (#9978).""" - self.client.logout() - login = self.client.login(username='testclient',password='password') - self.assertTrue(login, 'Could not log in') - self.client.logout() - self.client.logout() - -class RequestMethodTests(TestCase): - def test_get(self): - "Request a view via request method GET" - response = self.client.get('/test_client_regress/request_methods/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'request method: GET') - - def test_post(self): - "Request a view via request method POST" - response = self.client.post('/test_client_regress/request_methods/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'request method: POST') - - def test_head(self): - "Request a view via request method HEAD" - response = self.client.head('/test_client_regress/request_methods/') - self.assertEqual(response.status_code, 200) - # A HEAD request doesn't return any content. - self.assertNotEqual(response.content, 'request method: HEAD') - self.assertEqual(response.content, '') - - def test_options(self): - "Request a view via request method OPTIONS" - response = self.client.options('/test_client_regress/request_methods/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'request method: OPTIONS') - - def test_put(self): - "Request a view via request method PUT" - response = self.client.put('/test_client_regress/request_methods/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'request method: PUT') - - def test_delete(self): - "Request a view via request method DELETE" - response = self.client.delete('/test_client_regress/request_methods/') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'request method: DELETE') - -class RequestMethodStringDataTests(TestCase): - def test_post(self): - "Request a view with string data via request method POST" - # Regression test for #11371 - data = u'{"test": "json"}' - response = self.client.post('/test_client_regress/request_methods/', data=data, content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'request method: POST') - - def test_put(self): - "Request a view with string data via request method PUT" - # Regression test for #11371 - data = u'{"test": "json"}' - response = self.client.put('/test_client_regress/request_methods/', data=data, content_type='application/json') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.content, 'request method: PUT') - -class QueryStringTests(TestCase): - def test_get_like_requests(self): - # See: https://code.djangoproject.com/ticket/10571. - # Removed 'put' and 'delete' here as they are 'GET-like requests' - for method_name in ('get','head','options'): - # A GET-like request can pass a query string as data - method = getattr(self.client, method_name) - response = method("/test_client_regress/request_data/", data={'foo':'whiz'}) - self.assertEqual(response.context['get-foo'], 'whiz') - self.assertEqual(response.context['request-foo'], 'whiz') - - # A GET-like request can pass a query string as part of the URL - response = method("/test_client_regress/request_data/?foo=whiz") - self.assertEqual(response.context['get-foo'], 'whiz') - self.assertEqual(response.context['request-foo'], 'whiz') - - # Data provided in the URL to a GET-like request is overridden by actual form data - response = method("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) - self.assertEqual(response.context['get-foo'], 'bang') - self.assertEqual(response.context['request-foo'], 'bang') - - response = method("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) - self.assertEqual(response.context['get-foo'], None) - self.assertEqual(response.context['get-bar'], 'bang') - self.assertEqual(response.context['request-foo'], None) - self.assertEqual(response.context['request-bar'], 'bang') - - def test_post_like_requests(self): - # A POST-like request can pass a query string as data - response = self.client.post("/test_client_regress/request_data/", data={'foo':'whiz'}) - self.assertEqual(response.context['get-foo'], None) - self.assertEqual(response.context['post-foo'], 'whiz') - - # A POST-like request can pass a query string as part of the URL - response = self.client.post("/test_client_regress/request_data/?foo=whiz") - self.assertEqual(response.context['get-foo'], 'whiz') - self.assertEqual(response.context['post-foo'], None) - self.assertEqual(response.context['request-foo'], 'whiz') - - # POST data provided in the URL augments actual form data - response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) - self.assertEqual(response.context['get-foo'], 'whiz') - self.assertEqual(response.context['post-foo'], 'bang') - self.assertEqual(response.context['request-foo'], 'bang') - - response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) - self.assertEqual(response.context['get-foo'], 'whiz') - self.assertEqual(response.context['get-bar'], None) - self.assertEqual(response.context['post-foo'], None) - self.assertEqual(response.context['post-bar'], 'bang') - self.assertEqual(response.context['request-foo'], 'whiz') - self.assertEqual(response.context['request-bar'], 'bang') - -class UnicodePayloadTests(TestCase): - def test_simple_unicode_payload(self): - "A simple ASCII-only unicode JSON document can be POSTed" - # Regression test for #10571 - json = u'{"english": "mountain pass"}' - response = self.client.post("/test_client_regress/parse_unicode_json/", json, - content_type="application/json") - self.assertEqual(response.content, json) - response = self.client.put("/test_client_regress/parse_unicode_json/", json, - content_type="application/json") - self.assertEqual(response.content, json) - - def test_unicode_payload_utf8(self): - "A non-ASCII unicode data encoded as UTF-8 can be POSTed" - # Regression test for #10571 - json = u'{"dog": "собака"}' - response = self.client.post("/test_client_regress/parse_unicode_json/", json, - content_type="application/json; charset=utf-8") - self.assertEqual(response.content, json.encode('utf-8')) - response = self.client.put("/test_client_regress/parse_unicode_json/", json, - content_type="application/json; charset=utf-8") - self.assertEqual(response.content, json.encode('utf-8')) - - def test_unicode_payload_utf16(self): - "A non-ASCII unicode data encoded as UTF-16 can be POSTed" - # Regression test for #10571 - json = u'{"dog": "собака"}' - response = self.client.post("/test_client_regress/parse_unicode_json/", json, - content_type="application/json; charset=utf-16") - self.assertEqual(response.content, json.encode('utf-16')) - response = self.client.put("/test_client_regress/parse_unicode_json/", json, - content_type="application/json; charset=utf-16") - self.assertEqual(response.content, json.encode('utf-16')) - - def test_unicode_payload_non_utf(self): - "A non-ASCII unicode data as a non-UTF based encoding can be POSTed" - #Regression test for #10571 - json = u'{"dog": "собака"}' - response = self.client.post("/test_client_regress/parse_unicode_json/", json, - content_type="application/json; charset=koi8-r") - self.assertEqual(response.content, json.encode('koi8-r')) - response = self.client.put("/test_client_regress/parse_unicode_json/", json, - content_type="application/json; charset=koi8-r") - self.assertEqual(response.content, json.encode('koi8-r')) - -class DummyFile(object): - def __init__(self, filename): - self.name = filename - def read(self): - return 'TEST_FILE_CONTENT' - -class UploadedFileEncodingTest(TestCase): - def test_file_encoding(self): - encoded_file = encode_file('TEST_BOUNDARY', 'TEST_KEY', DummyFile('test_name.bin')) - self.assertEqual('--TEST_BOUNDARY', encoded_file[0]) - self.assertEqual('Content-Disposition: form-data; name="TEST_KEY"; filename="test_name.bin"', encoded_file[1]) - self.assertEqual('TEST_FILE_CONTENT', encoded_file[-1]) - - def test_guesses_content_type_on_file_encoding(self): - self.assertEqual('Content-Type: application/octet-stream', - encode_file('IGNORE', 'IGNORE', DummyFile("file.bin"))[2]) - self.assertEqual('Content-Type: text/plain', - encode_file('IGNORE', 'IGNORE', DummyFile("file.txt"))[2]) - self.assertIn(encode_file('IGNORE', 'IGNORE', DummyFile("file.zip"))[2], ( - 'Content-Type: application/x-compress', - 'Content-Type: application/x-zip', - 'Content-Type: application/x-zip-compressed', - 'Content-Type: application/zip',)) - self.assertEqual('Content-Type: application/octet-stream', - encode_file('IGNORE', 'IGNORE', DummyFile("file.unknown"))[2]) - -class RequestHeadersTest(TestCase): - def test_client_headers(self): - "A test client can receive custom headers" - response = self.client.get("/test_client_regress/check_headers/", HTTP_X_ARG_CHECK='Testing 123') - self.assertEqual(response.content, "HTTP_X_ARG_CHECK: Testing 123") - self.assertEqual(response.status_code, 200) - - def test_client_headers_redirect(self): - "Test client headers are preserved through redirects" - response = self.client.get("/test_client_regress/check_headers_redirect/", follow=True, HTTP_X_ARG_CHECK='Testing 123') - self.assertEqual(response.content, "HTTP_X_ARG_CHECK: Testing 123") - self.assertRedirects(response, '/test_client_regress/check_headers/', - status_code=301, target_status_code=200) - - -class ReadLimitedStreamTest(TestCase): - """ - Tests that ensure that HttpRequest.body, HttpRequest.read() and - HttpRequest.read(BUFFER) have proper LimitedStream behavior. - - Refs #14753, #15785 - """ - - def test_body_from_empty_request(self): - """HttpRequest.body on a test client GET request should return - the empty string.""" - self.assertEqual(self.client.get("/test_client_regress/body/").content, '') - - def test_read_from_empty_request(self): - """HttpRequest.read() on a test client GET request should return the - empty string.""" - self.assertEqual(self.client.get("/test_client_regress/read_all/").content, '') - - def test_read_numbytes_from_empty_request(self): - """HttpRequest.read(LARGE_BUFFER) on a test client GET request should - return the empty string.""" - self.assertEqual(self.client.get("/test_client_regress/read_buffer/").content, '') - - def test_read_from_nonempty_request(self): - """HttpRequest.read() on a test client PUT request with some payload - should return that payload.""" - payload = 'foobar' - self.assertEqual(self.client.put("/test_client_regress/read_all/", - data=payload, - content_type='text/plain').content, payload) - - def test_read_numbytes_from_nonempty_request(self): - """HttpRequest.read(LARGE_BUFFER) on a test client PUT request with - some payload should return that payload.""" - payload = 'foobar' - self.assertEqual(self.client.put("/test_client_regress/read_buffer/", - data=payload, - content_type='text/plain').content, payload) - - -class RequestFactoryStateTest(TestCase): - """Regression tests for #15929.""" - # These tests are checking that certain middleware don't change certain - # global state. Alternatively, from the point of view of a test, they are - # ensuring test isolation behavior. So, unusually, it doesn't make sense to - # run the tests individually, and if any are failing it is confusing to run - # them with any other set of tests. - - def setUp(self): - self.factory = RequestFactory() - - def common_test_that_should_always_pass(self): - request = self.factory.get('/') - request.session = {} - self.assertFalse(hasattr(request, 'user')) - - def test_request(self): - self.common_test_that_should_always_pass() - - def test_request_after_client(self): - # apart from the next line the three tests are identical - self.client.get('/') - self.common_test_that_should_always_pass() - - def test_request_after_client_2(self): - # This test is executed after the previous one - self.common_test_that_should_always_pass() - - -class RequestFactoryEnvironmentTests(TestCase): - """ - Regression tests for #8551 and #17067: ensure that environment variables - are set correctly in RequestFactory. - """ - - def setUp(self): - self.factory = RequestFactory() - - def test_should_set_correct_env_variables(self): - request = self.factory.get('/path/') - - self.assertEqual(request.META.get('REMOTE_ADDR'), '127.0.0.1') - self.assertEqual(request.META.get('SERVER_NAME'), 'testserver') - self.assertEqual(request.META.get('SERVER_PORT'), '80') - self.assertEqual(request.META.get('SERVER_PROTOCOL'), 'HTTP/1.1') - self.assertEqual(request.META.get('SCRIPT_NAME') + - request.META.get('PATH_INFO'), '/path/') diff --git a/tests/regressiontests/test_client_regress/tests.py b/tests/regressiontests/test_client_regress/tests.py new file mode 100644 index 0000000000..ece487a211 --- /dev/null +++ b/tests/regressiontests/test_client_regress/tests.py @@ -0,0 +1,1006 @@ +# -*- coding: utf-8 -*- +""" +Regression tests for the Test Client, especially the customized assertions. +""" +import os + +from django.conf import settings +from django.core.exceptions import SuspiciousOperation +from django.core.urlresolvers import reverse +from django.template import (TemplateDoesNotExist, TemplateSyntaxError, + Context, Template, loader) +import django.template.context +from django.test import Client, TestCase +from django.test.client import encode_file, RequestFactory +from django.test.utils import ContextList, override_settings +from django.template.response import SimpleTemplateResponse +from django.http import HttpResponse + + +@override_settings( + TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'templates'),) +) +class AssertContainsTests(TestCase): + def test_contains(self): + "Responses can be inspected for content, including counting repeated substrings" + response = self.client.get('/test_client_regress/no_template_view/') + + self.assertNotContains(response, 'never') + self.assertContains(response, 'never', 0) + self.assertContains(response, 'once') + self.assertContains(response, 'once', 1) + self.assertContains(response, 'twice') + self.assertContains(response, 'twice', 2) + + try: + self.assertContains(response, 'text', status_code=999) + except AssertionError as e: + self.assertIn("Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) + try: + self.assertContains(response, 'text', status_code=999, msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) + + try: + self.assertNotContains(response, 'text', status_code=999) + except AssertionError as e: + self.assertIn("Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) + try: + self.assertNotContains(response, 'text', status_code=999, msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Couldn't retrieve content: Response code was 200 (expected 999)", str(e)) + + try: + self.assertNotContains(response, 'once') + except AssertionError as e: + self.assertIn("Response should not contain 'once'", str(e)) + try: + self.assertNotContains(response, 'once', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Response should not contain 'once'", str(e)) + + try: + self.assertContains(response, 'never', 1) + except AssertionError as e: + self.assertIn("Found 0 instances of 'never' in response (expected 1)", str(e)) + try: + self.assertContains(response, 'never', 1, msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Found 0 instances of 'never' in response (expected 1)", str(e)) + + try: + self.assertContains(response, 'once', 0) + except AssertionError as e: + self.assertIn("Found 1 instances of 'once' in response (expected 0)", str(e)) + try: + self.assertContains(response, 'once', 0, msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Found 1 instances of 'once' in response (expected 0)", str(e)) + + try: + self.assertContains(response, 'once', 2) + except AssertionError as e: + self.assertIn("Found 1 instances of 'once' in response (expected 2)", str(e)) + try: + self.assertContains(response, 'once', 2, msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Found 1 instances of 'once' in response (expected 2)", str(e)) + + try: + self.assertContains(response, 'twice', 1) + except AssertionError as e: + self.assertIn("Found 2 instances of 'twice' in response (expected 1)", str(e)) + try: + self.assertContains(response, 'twice', 1, msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Found 2 instances of 'twice' in response (expected 1)", str(e)) + + try: + self.assertContains(response, 'thrice') + except AssertionError as e: + self.assertIn("Couldn't find 'thrice' in response", str(e)) + try: + self.assertContains(response, 'thrice', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Couldn't find 'thrice' in response", str(e)) + + try: + self.assertContains(response, 'thrice', 3) + except AssertionError as e: + self.assertIn("Found 0 instances of 'thrice' in response (expected 3)", str(e)) + try: + self.assertContains(response, 'thrice', 3, msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Found 0 instances of 'thrice' in response (expected 3)", str(e)) + + def test_unicode_contains(self): + "Unicode characters can be found in template context" + #Regression test for #10183 + r = self.client.get('/test_client_regress/check_unicode/') + self.assertContains(r, u'さかき') + self.assertContains(r, b'\xe5\xb3\xa0'.decode('utf-8')) + + def test_unicode_not_contains(self): + "Unicode characters can be searched for, and not found in template context" + #Regression test for #10183 + r = self.client.get('/test_client_regress/check_unicode/') + self.assertNotContains(r, u'はたけ') + self.assertNotContains(r, b'\xe3\x81\xaf\xe3\x81\x9f\xe3\x81\x91'.decode('utf-8')) + + def test_assert_contains_renders_template_response(self): + """ Test that we can pass in an unrendered SimpleTemplateReponse + without throwing an error. + Refs #15826. + """ + response = SimpleTemplateResponse(Template('Hello'), status=200) + self.assertContains(response, 'Hello') + + def test_assert_contains_using_non_template_response(self): + """ Test that auto-rendering does not affect responses that aren't + instances (or subclasses) of SimpleTemplateResponse. + Refs #15826. + """ + response = HttpResponse('Hello') + self.assertContains(response, 'Hello') + + def test_assert_not_contains_renders_template_response(self): + """ Test that we can pass in an unrendered SimpleTemplateReponse + without throwing an error. + Refs #15826. + """ + response = SimpleTemplateResponse(Template('Hello'), status=200) + self.assertNotContains(response, 'Bye') + + def test_assert_not_contains_using_non_template_response(self): + """ Test that auto-rendering does not affect responses that aren't + instances (or subclasses) of SimpleTemplateResponse. + Refs #15826. + """ + response = HttpResponse('Hello') + self.assertNotContains(response, 'Bye') + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class AssertTemplateUsedTests(TestCase): + fixtures = ['testdata.json'] + + def test_no_context(self): + "Template usage assertions work then templates aren't in use" + response = self.client.get('/test_client_regress/no_template_view/') + + # Check that the no template case doesn't mess with the template assertions + self.assertTemplateNotUsed(response, 'GET Template') + + try: + self.assertTemplateUsed(response, 'GET Template') + except AssertionError as e: + self.assertIn("No templates used to render the response", str(e)) + + try: + self.assertTemplateUsed(response, 'GET Template', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: No templates used to render the response", str(e)) + + def test_single_context(self): + "Template assertions work when there is a single context" + response = self.client.get('/test_client/post_view/', {}) + + try: + self.assertTemplateNotUsed(response, 'Empty GET Template') + except AssertionError as e: + self.assertIn("Template 'Empty GET Template' was used unexpectedly in rendering the response", str(e)) + + try: + self.assertTemplateNotUsed(response, 'Empty GET Template', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Template 'Empty GET Template' was used unexpectedly in rendering the response", str(e)) + + try: + self.assertTemplateUsed(response, 'Empty POST Template') + except AssertionError as e: + self.assertIn("Template 'Empty POST Template' was not a template used to render the response. Actual template(s) used: Empty GET Template", str(e)) + + try: + self.assertTemplateUsed(response, 'Empty POST Template', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Template 'Empty POST Template' was not a template used to render the response. Actual template(s) used: Empty GET Template", str(e)) + + def test_multiple_context(self): + "Template assertions work when there are multiple contexts" + post_data = { + 'text': 'Hello World', + 'email': 'foo@example.com', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view_with_template/', post_data) + self.assertContains(response, 'POST data OK') + try: + self.assertTemplateNotUsed(response, "form_view.html") + except AssertionError as e: + self.assertIn("Template 'form_view.html' was used unexpectedly in rendering the response", str(e)) + + try: + self.assertTemplateNotUsed(response, 'base.html') + except AssertionError as e: + self.assertIn("Template 'base.html' was used unexpectedly in rendering the response", str(e)) + + try: + self.assertTemplateUsed(response, "Valid POST Template") + except AssertionError as e: + self.assertIn("Template 'Valid POST Template' was not a template used to render the response. Actual template(s) used: form_view.html, base.html", str(e)) + +class AssertRedirectsTests(TestCase): + def test_redirect_page(self): + "An assertion is raised if the original page couldn't be retrieved as expected" + # This page will redirect with code 301, not 302 + response = self.client.get('/test_client/permanent_redirect_view/') + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError as e: + self.assertIn("Response didn't redirect as expected: Response code was 301 (expected 302)", str(e)) + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Response didn't redirect as expected: Response code was 301 (expected 302)", str(e)) + + def test_lost_query(self): + "An assertion is raised if the redirect location doesn't preserve GET parameters" + response = self.client.get('/test_client/redirect_view/', {'var': 'value'}) + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError as e: + self.assertIn("Response redirected to 'http://testserver/test_client/get_view/?var=value', expected 'http://testserver/test_client/get_view/'", str(e)) + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Response redirected to 'http://testserver/test_client/get_view/?var=value', expected 'http://testserver/test_client/get_view/'", str(e)) + + def test_incorrect_target(self): + "An assertion is raised if the response redirects to another target" + response = self.client.get('/test_client/permanent_redirect_view/') + try: + # Should redirect to get_view + self.assertRedirects(response, '/test_client/some_view/') + except AssertionError as e: + self.assertIn("Response didn't redirect as expected: Response code was 301 (expected 302)", str(e)) + + def test_target_page(self): + "An assertion is raised if the response redirect target cannot be retrieved as expected" + response = self.client.get('/test_client/double_redirect_view/') + try: + # The redirect target responds with a 301 code, not 200 + self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/') + except AssertionError as e: + self.assertIn("Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)", str(e)) + + try: + # The redirect target responds with a 301 code, not 200 + self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)", str(e)) + + def test_redirect_chain(self): + "You can follow a redirect chain of multiple redirects" + response = self.client.get('/test_client_regress/redirects/further/more/', {}, follow=True) + self.assertRedirects(response, '/test_client_regress/no_template_view/', + status_code=301, target_status_code=200) + + self.assertEqual(len(response.redirect_chain), 1) + self.assertEqual(response.redirect_chain[0], ('http://testserver/test_client_regress/no_template_view/', 301)) + + def test_multiple_redirect_chain(self): + "You can follow a redirect chain of multiple redirects" + response = self.client.get('/test_client_regress/redirects/', {}, follow=True) + self.assertRedirects(response, '/test_client_regress/no_template_view/', + status_code=301, target_status_code=200) + + self.assertEqual(len(response.redirect_chain), 3) + self.assertEqual(response.redirect_chain[0], ('http://testserver/test_client_regress/redirects/further/', 301)) + self.assertEqual(response.redirect_chain[1], ('http://testserver/test_client_regress/redirects/further/more/', 301)) + self.assertEqual(response.redirect_chain[2], ('http://testserver/test_client_regress/no_template_view/', 301)) + + def test_redirect_chain_to_non_existent(self): + "You can follow a chain to a non-existent view" + response = self.client.get('/test_client_regress/redirect_to_non_existent_view2/', {}, follow=True) + self.assertRedirects(response, '/test_client_regress/non_existent_view/', + status_code=301, target_status_code=404) + + def test_redirect_chain_to_self(self): + "Redirections to self are caught and escaped" + response = self.client.get('/test_client_regress/redirect_to_self/', {}, follow=True) + # The chain of redirects stops once the cycle is detected. + self.assertRedirects(response, '/test_client_regress/redirect_to_self/', + status_code=301, target_status_code=301) + self.assertEqual(len(response.redirect_chain), 2) + + def test_circular_redirect(self): + "Circular redirect chains are caught and escaped" + response = self.client.get('/test_client_regress/circular_redirect_1/', {}, follow=True) + # The chain of redirects will get back to the starting point, but stop there. + self.assertRedirects(response, '/test_client_regress/circular_redirect_2/', + status_code=301, target_status_code=301) + self.assertEqual(len(response.redirect_chain), 4) + + def test_redirect_chain_post(self): + "A redirect chain will be followed from an initial POST post" + response = self.client.post('/test_client_regress/redirects/', + {'nothing': 'to_send'}, follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEqual(len(response.redirect_chain), 3) + + def test_redirect_chain_head(self): + "A redirect chain will be followed from an initial HEAD request" + response = self.client.head('/test_client_regress/redirects/', + {'nothing': 'to_send'}, follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEqual(len(response.redirect_chain), 3) + + def test_redirect_chain_options(self): + "A redirect chain will be followed from an initial OPTIONS request" + response = self.client.options('/test_client_regress/redirects/', + follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEqual(len(response.redirect_chain), 3) + + def test_redirect_chain_put(self): + "A redirect chain will be followed from an initial PUT request" + response = self.client.put('/test_client_regress/redirects/', + follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEqual(len(response.redirect_chain), 3) + + def test_redirect_chain_delete(self): + "A redirect chain will be followed from an initial DELETE request" + response = self.client.delete('/test_client_regress/redirects/', + follow=True) + self.assertRedirects(response, + '/test_client_regress/no_template_view/', 301, 200) + self.assertEqual(len(response.redirect_chain), 3) + + def test_redirect_to_different_host(self): + "The test client will preserve scheme, host and port changes" + response = self.client.get('/test_client_regress/redirect_other_host/', follow=True) + self.assertRedirects(response, + 'https://otherserver:8443/test_client_regress/no_template_view/', + status_code=301, target_status_code=200) + # We can't use is_secure() or get_host() + # because response.request is a dictionary, not an HttpRequest + self.assertEqual(response.request.get('wsgi.url_scheme'), 'https') + self.assertEqual(response.request.get('SERVER_NAME'), 'otherserver') + self.assertEqual(response.request.get('SERVER_PORT'), '8443') + + def test_redirect_chain_on_non_redirect_page(self): + "An assertion is raised if the original page couldn't be retrieved as expected" + # This page will redirect with code 301, not 302 + response = self.client.get('/test_client/get_view/', follow=True) + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError as e: + self.assertIn("Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) + + def test_redirect_on_non_redirect_page(self): + "An assertion is raised if the original page couldn't be retrieved as expected" + # This page will redirect with code 301, not 302 + response = self.client.get('/test_client/get_view/') + try: + self.assertRedirects(response, '/test_client/get_view/') + except AssertionError as e: + self.assertIn("Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) + + try: + self.assertRedirects(response, '/test_client/get_view/', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: Response didn't redirect as expected: Response code was 200 (expected 302)", str(e)) + + +class AssertFormErrorTests(TestCase): + def test_unknown_form(self): + "An assertion is raised if the form name is unknown" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'wrong_form', 'some_field', 'Some error.') + except AssertionError as e: + self.assertIn("The form 'wrong_form' was not used to render the response", str(e)) + try: + self.assertFormError(response, 'wrong_form', 'some_field', 'Some error.', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: The form 'wrong_form' was not used to render the response", str(e)) + + def test_unknown_field(self): + "An assertion is raised if the field name is unknown" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', 'some_field', 'Some error.') + except AssertionError as e: + self.assertIn("The form 'form' in context 0 does not contain the field 'some_field'", str(e)) + try: + self.assertFormError(response, 'form', 'some_field', 'Some error.', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: The form 'form' in context 0 does not contain the field 'some_field'", str(e)) + + def test_noerror_field(self): + "An assertion is raised if the field doesn't have any errors" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', 'value', 'Some error.') + except AssertionError as e: + self.assertIn("The field 'value' on form 'form' in context 0 contains no errors", str(e)) + try: + self.assertFormError(response, 'form', 'value', 'Some error.', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: The field 'value' on form 'form' in context 0 contains no errors", str(e)) + + def test_unknown_error(self): + "An assertion is raised if the field doesn't contain the provided error" + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', 'email', 'Some error.') + except AssertionError as e: + self.assertIn("The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [u'Enter a valid e-mail address.'])", str(e)) + try: + self.assertFormError(response, 'form', 'email', 'Some error.', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: The field 'email' on form 'form' in context 0 does not contain the error 'Some error.' (actual errors: [u'Enter a valid e-mail address.'])", str(e)) + + def test_unknown_nonfield_error(self): + """ + Checks that an assertion is raised if the form's non field errors + doesn't contain the provided error. + """ + post_data = { + 'text': 'Hello World', + 'email': 'not an email address', + 'value': 37, + 'single': 'b', + 'multi': ('b','c','e') + } + response = self.client.post('/test_client/form_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + + try: + self.assertFormError(response, 'form', None, 'Some error.') + except AssertionError as e: + self.assertIn("The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e)) + try: + self.assertFormError(response, 'form', None, 'Some error.', msg_prefix='abc') + except AssertionError as e: + self.assertIn("abc: The form 'form' in context 0 does not contain the non-field error 'Some error.' (actual errors: )", str(e)) + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class LoginTests(TestCase): + fixtures = ['testdata'] + + def test_login_different_client(self): + "Check that using a different test client doesn't violate authentication" + + # Create a second client, and log in. + c = Client() + login = c.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Get a redirection page with the second client. + response = c.get("/test_client_regress/login_protected_redirect_view/") + + # At this points, the self.client isn't logged in. + # Check that assertRedirects uses the original client, not the + # default client. + self.assertRedirects(response, "http://testserver/test_client_regress/get_view/") + + +@override_settings( + PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',), + SESSION_ENGINE='regressiontests.test_client_regress.session' +) +class SessionEngineTests(TestCase): + fixtures = ['testdata'] + + def test_login(self): + "A session engine that modifies the session key can be used to log in" + login = self.client.login(username='testclient', password='password') + self.assertTrue(login, 'Could not log in') + + # Try to access a login protected page. + response = self.client.get("/test_client/login_protected_view/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['user'].username, 'testclient') + + +class URLEscapingTests(TestCase): + def test_simple_argument_get(self): + "Get a view that has a simple string argument" + response = self.client.get(reverse('arg_view', args=['Slartibartfast'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'Howdy, Slartibartfast') + + def test_argument_with_space_get(self): + "Get a view that has a string argument that requires escaping" + response = self.client.get(reverse('arg_view', args=['Arthur Dent'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'Hi, Arthur') + + def test_simple_argument_post(self): + "Post for a view that has a simple string argument" + response = self.client.post(reverse('arg_view', args=['Slartibartfast'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'Howdy, Slartibartfast') + + def test_argument_with_space_post(self): + "Post for a view that has a string argument that requires escaping" + response = self.client.post(reverse('arg_view', args=['Arthur Dent'])) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'Hi, Arthur') + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class ExceptionTests(TestCase): + fixtures = ['testdata.json'] + + def test_exception_cleared(self): + "#5836 - A stale user exception isn't re-raised by the test client." + + login = self.client.login(username='testclient',password='password') + self.assertTrue(login, 'Could not log in') + try: + response = self.client.get("/test_client_regress/staff_only/") + self.fail("General users should not be able to visit this page") + except SuspiciousOperation: + pass + + # At this point, an exception has been raised, and should be cleared. + + # This next operation should be successful; if it isn't we have a problem. + login = self.client.login(username='staff', password='password') + self.assertTrue(login, 'Could not log in') + try: + self.client.get("/test_client_regress/staff_only/") + except SuspiciousOperation: + self.fail("Staff should be able to visit this page") + + +class TemplateExceptionTests(TestCase): + def setUp(self): + # Reset the loaders so they don't try to render cached templates. + if loader.template_source_loaders is not None: + for template_loader in loader.template_source_loaders: + if hasattr(template_loader, 'reset'): + template_loader.reset() + + @override_settings(TEMPLATE_DIRS=(),) + def test_no_404_template(self): + "Missing templates are correctly reported by test client" + try: + response = self.client.get("/no_such_view/") + self.fail("Should get error about missing template") + except TemplateDoesNotExist: + pass + + @override_settings( + TEMPLATE_DIRS=(os.path.join(os.path.dirname(__file__), 'bad_templates'),) + ) + def test_bad_404_template(self): + "Errors found when rendering 404 error templates are re-raised" + try: + response = self.client.get("/no_such_view/") + self.fail("Should get error about syntax error in template") + except TemplateSyntaxError: + pass + +# We need two different tests to check URLconf substitution - one to check +# it was changed, and another one (without self.urls) to check it was reverted on +# teardown. This pair of tests relies upon the alphabetical ordering of test execution. +class UrlconfSubstitutionTests(TestCase): + urls = 'regressiontests.test_client_regress.urls' + + def test_urlconf_was_changed(self): + "TestCase can enforce a custom URLconf on a per-test basis" + url = reverse('arg_view', args=['somename']) + self.assertEqual(url, '/arg_view/somename/') + +# This test needs to run *after* UrlconfSubstitutionTests; the zz prefix in the +# name is to ensure alphabetical ordering. +class zzUrlconfSubstitutionTests(TestCase): + def test_urlconf_was_reverted(self): + "URLconf is reverted to original value after modification in a TestCase" + url = reverse('arg_view', args=['somename']) + self.assertEqual(url, '/test_client_regress/arg_view/somename/') + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class ContextTests(TestCase): + fixtures = ['testdata'] + + def test_single_context(self): + "Context variables can be retrieved from a single context" + response = self.client.get("/test_client_regress/request_data/", data={'foo':'whiz'}) + self.assertEqual(response.context.__class__, Context) + self.assertTrue('get-foo' in response.context) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + self.assertEqual(response.context['data'], 'sausage') + + try: + response.context['does-not-exist'] + self.fail('Should not be able to retrieve non-existent key') + except KeyError as e: + self.assertEqual(e.args[0], 'does-not-exist') + + def test_inherited_context(self): + "Context variables can be retrieved from a list of contexts" + response = self.client.get("/test_client_regress/request_data_extended/", data={'foo':'whiz'}) + self.assertEqual(response.context.__class__, ContextList) + self.assertEqual(len(response.context), 2) + self.assertTrue('get-foo' in response.context) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + self.assertEqual(response.context['data'], 'bacon') + + try: + response.context['does-not-exist'] + self.fail('Should not be able to retrieve non-existent key') + except KeyError as e: + self.assertEqual(e.args[0], 'does-not-exist') + + def test_15368(self): + # Need to insert a context processor that assumes certain things about + # the request instance. This triggers a bug caused by some ways of + # copying RequestContext. + try: + django.template.context._standard_context_processors = (lambda request: {'path': request.special_path},) + response = self.client.get("/test_client_regress/request_context_view/") + self.assertContains(response, 'Path: /test_client_regress/request_context_view/') + finally: + django.template.context._standard_context_processors = None + + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class SessionTests(TestCase): + fixtures = ['testdata.json'] + + def test_session(self): + "The session isn't lost if a user logs in" + # The session doesn't exist to start. + response = self.client.get('/test_client_regress/check_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'NO') + + # This request sets a session variable. + response = self.client.get('/test_client_regress/set_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'set_session') + + # Check that the session has been modified + response = self.client.get('/test_client_regress/check_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'YES') + + # Log in + login = self.client.login(username='testclient',password='password') + self.assertTrue(login, 'Could not log in') + + # Session should still contain the modified value + response = self.client.get('/test_client_regress/check_session/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'YES') + + def test_logout(self): + """Logout should work whether the user is logged in or not (#9978).""" + self.client.logout() + login = self.client.login(username='testclient',password='password') + self.assertTrue(login, 'Could not log in') + self.client.logout() + self.client.logout() + +class RequestMethodTests(TestCase): + def test_get(self): + "Request a view via request method GET" + response = self.client.get('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'request method: GET') + + def test_post(self): + "Request a view via request method POST" + response = self.client.post('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'request method: POST') + + def test_head(self): + "Request a view via request method HEAD" + response = self.client.head('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + # A HEAD request doesn't return any content. + self.assertNotEqual(response.content, b'request method: HEAD') + self.assertEqual(response.content, b'') + + def test_options(self): + "Request a view via request method OPTIONS" + response = self.client.options('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'request method: OPTIONS') + + def test_put(self): + "Request a view via request method PUT" + response = self.client.put('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'request method: PUT') + + def test_delete(self): + "Request a view via request method DELETE" + response = self.client.delete('/test_client_regress/request_methods/') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'request method: DELETE') + +class RequestMethodStringDataTests(TestCase): + def test_post(self): + "Request a view with string data via request method POST" + # Regression test for #11371 + data = u'{"test": "json"}' + response = self.client.post('/test_client_regress/request_methods/', data=data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'request method: POST') + + def test_put(self): + "Request a view with string data via request method PUT" + # Regression test for #11371 + data = u'{"test": "json"}' + response = self.client.put('/test_client_regress/request_methods/', data=data, content_type='application/json') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'request method: PUT') + +class QueryStringTests(TestCase): + def test_get_like_requests(self): + # See: https://code.djangoproject.com/ticket/10571. + for method_name in ('get', 'head'): + # A GET-like request can pass a query string as data + method = getattr(self.client, method_name) + response = method("/test_client_regress/request_data/", data={'foo':'whiz'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + + # A GET-like request can pass a query string as part of the URL + response = method("/test_client_regress/request_data/?foo=whiz") + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['request-foo'], 'whiz') + + # Data provided in the URL to a GET-like request is overridden by actual form data + response = method("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) + self.assertEqual(response.context['get-foo'], 'bang') + self.assertEqual(response.context['request-foo'], 'bang') + + response = method("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) + self.assertEqual(response.context['get-foo'], None) + self.assertEqual(response.context['get-bar'], 'bang') + self.assertEqual(response.context['request-foo'], None) + self.assertEqual(response.context['request-bar'], 'bang') + + def test_post_like_requests(self): + # A POST-like request can pass a query string as data + response = self.client.post("/test_client_regress/request_data/", data={'foo':'whiz'}) + self.assertEqual(response.context['get-foo'], None) + self.assertEqual(response.context['post-foo'], 'whiz') + + # A POST-like request can pass a query string as part of the URL + response = self.client.post("/test_client_regress/request_data/?foo=whiz") + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['post-foo'], None) + self.assertEqual(response.context['request-foo'], 'whiz') + + # POST data provided in the URL augments actual form data + response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'foo':'bang'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['post-foo'], 'bang') + self.assertEqual(response.context['request-foo'], 'bang') + + response = self.client.post("/test_client_regress/request_data/?foo=whiz", data={'bar':'bang'}) + self.assertEqual(response.context['get-foo'], 'whiz') + self.assertEqual(response.context['get-bar'], None) + self.assertEqual(response.context['post-foo'], None) + self.assertEqual(response.context['post-bar'], 'bang') + self.assertEqual(response.context['request-foo'], 'whiz') + self.assertEqual(response.context['request-bar'], 'bang') + +class UnicodePayloadTests(TestCase): + def test_simple_unicode_payload(self): + "A simple ASCII-only unicode JSON document can be POSTed" + # Regression test for #10571 + json = u'{"english": "mountain pass"}' + response = self.client.post("/test_client_regress/parse_unicode_json/", json, + content_type="application/json") + self.assertEqual(response.content, json) + + def test_unicode_payload_utf8(self): + "A non-ASCII unicode data encoded as UTF-8 can be POSTed" + # Regression test for #10571 + json = u'{"dog": "собака"}' + response = self.client.post("/test_client_regress/parse_unicode_json/", json, + content_type="application/json; charset=utf-8") + self.assertEqual(response.content, json.encode('utf-8')) + + def test_unicode_payload_utf16(self): + "A non-ASCII unicode data encoded as UTF-16 can be POSTed" + # Regression test for #10571 + json = u'{"dog": "собака"}' + response = self.client.post("/test_client_regress/parse_unicode_json/", json, + content_type="application/json; charset=utf-16") + self.assertEqual(response.content, json.encode('utf-16')) + + def test_unicode_payload_non_utf(self): + "A non-ASCII unicode data as a non-UTF based encoding can be POSTed" + #Regression test for #10571 + json = u'{"dog": "собака"}' + response = self.client.post("/test_client_regress/parse_unicode_json/", json, + content_type="application/json; charset=koi8-r") + self.assertEqual(response.content, json.encode('koi8-r')) + +class DummyFile(object): + def __init__(self, filename): + self.name = filename + def read(self): + return 'TEST_FILE_CONTENT' + +class UploadedFileEncodingTest(TestCase): + def test_file_encoding(self): + encoded_file = encode_file('TEST_BOUNDARY', 'TEST_KEY', DummyFile('test_name.bin')) + self.assertEqual(b'--TEST_BOUNDARY', encoded_file[0]) + self.assertEqual(b'Content-Disposition: form-data; name="TEST_KEY"; filename="test_name.bin"', encoded_file[1]) + self.assertEqual(b'TEST_FILE_CONTENT', encoded_file[-1]) + + def test_guesses_content_type_on_file_encoding(self): + self.assertEqual(b'Content-Type: application/octet-stream', + encode_file('IGNORE', 'IGNORE', DummyFile("file.bin"))[2]) + self.assertEqual(b'Content-Type: text/plain', + encode_file('IGNORE', 'IGNORE', DummyFile("file.txt"))[2]) + self.assertIn(encode_file('IGNORE', 'IGNORE', DummyFile("file.zip"))[2], ( + b'Content-Type: application/x-compress', + b'Content-Type: application/x-zip', + b'Content-Type: application/x-zip-compressed', + b'Content-Type: application/zip',)) + self.assertEqual(b'Content-Type: application/octet-stream', + encode_file('IGNORE', 'IGNORE', DummyFile("file.unknown"))[2]) + +class RequestHeadersTest(TestCase): + def test_client_headers(self): + "A test client can receive custom headers" + response = self.client.get("/test_client_regress/check_headers/", HTTP_X_ARG_CHECK='Testing 123') + self.assertEqual(response.content, b"HTTP_X_ARG_CHECK: Testing 123") + self.assertEqual(response.status_code, 200) + + def test_client_headers_redirect(self): + "Test client headers are preserved through redirects" + response = self.client.get("/test_client_regress/check_headers_redirect/", follow=True, HTTP_X_ARG_CHECK='Testing 123') + self.assertEqual(response.content, b"HTTP_X_ARG_CHECK: Testing 123") + self.assertRedirects(response, '/test_client_regress/check_headers/', + status_code=301, target_status_code=200) + + +class ReadLimitedStreamTest(TestCase): + """ + Tests that ensure that HttpRequest.body, HttpRequest.read() and + HttpRequest.read(BUFFER) have proper LimitedStream behavior. + + Refs #14753, #15785 + """ + + def test_body_from_empty_request(self): + """HttpRequest.body on a test client GET request should return + the empty string.""" + self.assertEqual(self.client.get("/test_client_regress/body/").content, b'') + + def test_read_from_empty_request(self): + """HttpRequest.read() on a test client GET request should return the + empty string.""" + self.assertEqual(self.client.get("/test_client_regress/read_all/").content, b'') + + def test_read_numbytes_from_empty_request(self): + """HttpRequest.read(LARGE_BUFFER) on a test client GET request should + return the empty string.""" + self.assertEqual(self.client.get("/test_client_regress/read_buffer/").content, b'') + + def test_read_from_nonempty_request(self): + """HttpRequest.read() on a test client PUT request with some payload + should return that payload.""" + payload = b'foobar' + self.assertEqual(self.client.put("/test_client_regress/read_all/", + data=payload, + content_type='text/plain').content, payload) + + def test_read_numbytes_from_nonempty_request(self): + """HttpRequest.read(LARGE_BUFFER) on a test client PUT request with + some payload should return that payload.""" + payload = b'foobar' + self.assertEqual(self.client.put("/test_client_regress/read_buffer/", + data=payload, + content_type='text/plain').content, payload) + + +class RequestFactoryStateTest(TestCase): + """Regression tests for #15929.""" + # These tests are checking that certain middleware don't change certain + # global state. Alternatively, from the point of view of a test, they are + # ensuring test isolation behavior. So, unusually, it doesn't make sense to + # run the tests individually, and if any are failing it is confusing to run + # them with any other set of tests. + + def common_test_that_should_always_pass(self): + request = RequestFactory().get('/') + request.session = {} + self.assertFalse(hasattr(request, 'user')) + + def test_request(self): + self.common_test_that_should_always_pass() + + def test_request_after_client(self): + # apart from the next line the three tests are identical + self.client.get('/') + self.common_test_that_should_always_pass() + + def test_request_after_client_2(self): + # This test is executed after the previous one + self.common_test_that_should_always_pass() + + +class RequestFactoryEnvironmentTests(TestCase): + """ + Regression tests for #8551 and #17067: ensure that environment variables + are set correctly in RequestFactory. + """ + + def test_should_set_correct_env_variables(self): + request = RequestFactory().get('/path/') + + self.assertEqual(request.META.get('REMOTE_ADDR'), '127.0.0.1') + self.assertEqual(request.META.get('SERVER_NAME'), 'testserver') + self.assertEqual(request.META.get('SERVER_PORT'), '80') + self.assertEqual(request.META.get('SERVER_PROTOCOL'), 'HTTP/1.1') + self.assertEqual(request.META.get('SCRIPT_NAME') + + request.META.get('PATH_INFO'), '/path/') diff --git a/tests/regressiontests/test_client_regress/views.py b/tests/regressiontests/test_client_regress/views.py index ab625a90dc..3a934ea047 100644 --- a/tests/regressiontests/test_client_regress/views.py +++ b/tests/regressiontests/test_client_regress/views.py @@ -5,7 +5,6 @@ from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect from django.core.exceptions import SuspiciousOperation from django.shortcuts import render_to_response -from django.utils.encoding import smart_str from django.core.serializers.json import DjangoJSONEncoder from django.test.client import CONTENT_TYPE_RE from django.template import RequestContext @@ -84,8 +83,8 @@ def return_json_file(request): obj_json = json.dumps(obj_dict, encoding=charset, cls=DjangoJSONEncoder, ensure_ascii=False) - response = HttpResponse(smart_str(obj_json, encoding=charset), status=200, - mimetype='application/json; charset=' + charset) + response = HttpResponse(obj_json.encode(charset), status=200, + mimetype='application/json; charset=%s' % charset) response['Content-Disposition'] = 'attachment; filename=testfile.json' return response diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py index 3a8a4efe3d..05f99105aa 100644 --- a/tests/regressiontests/test_runner/tests.py +++ b/tests/regressiontests/test_runner/tests.py @@ -8,8 +8,7 @@ from optparse import make_option from django.core.exceptions import ImproperlyConfigured from django.core.management import call_command from django import db -from django.db import connection -from django.test import simple, TransactionTestCase +from django.test import simple, TransactionTestCase, skipUnlessDBFeature from django.test.simple import DjangoTestSuiteRunner, get_tests from django.test.testcases import connections_support_transactions from django.utils import unittest @@ -291,16 +290,12 @@ class AutoIncrementResetTest(TransactionTestCase): and check that both times they get "1" as their PK value. That is, we test that AutoField values start from 1 for each transactional test case. """ - @unittest.skipIf(connection.vendor == 'oracle', - "Oracle's auto-increment fields are not reset between " - "tests") + @skipUnlessDBFeature('supports_sequence_reset') def test_autoincrement_reset1(self): p = Person.objects.create(first_name='Jack', last_name='Smith') self.assertEqual(p.pk, 1) - @unittest.skipIf(connection.vendor == 'oracle', - "Oracle's auto-increment fields are not reset between " - "tests") + @skipUnlessDBFeature('supports_sequence_reset') def test_autoincrement_reset2(self): p = Person.objects.create(first_name='Jack', last_name='Smith') self.assertEqual(p.pk, 1) diff --git a/tests/regressiontests/views/__init__.py b/tests/regressiontests/views/__init__.py index fa3db00113..1e12679af2 100644 --- a/tests/regressiontests/views/__init__.py +++ b/tests/regressiontests/views/__init__.py @@ -3,8 +3,8 @@ class BrokenException(Exception): pass -except_args = ('Broken!', # plain exception with ASCII text +except_args = (b'Broken!', # plain exception with ASCII text u'¡Broken!', # non-ASCII unicode data - '¡Broken!', # non-ASCII, utf-8 encoded bytestring - '\xa1Broken!', ) # non-ASCII, latin1 bytestring + u'¡Broken!'.encode('utf-8'), # non-ASCII, utf-8 encoded bytestring + b'\xa1Broken!', ) # non-ASCII, latin1 bytestring diff --git a/tests/regressiontests/views/tests/debug.py b/tests/regressiontests/views/tests/debug.py index c8358d334f..e8a7d49e79 100644 --- a/tests/regressiontests/views/tests/debug.py +++ b/tests/regressiontests/views/tests/debug.py @@ -10,13 +10,12 @@ from django.test import TestCase, RequestFactory from django.test.utils import (setup_test_template_loader, restore_template_loaders) from django.core.urlresolvers import reverse -from django.template import TemplateSyntaxError from django.views.debug import ExceptionReporter from django.core import mail from .. import BrokenException, except_args from ..views import (sensitive_view, non_sensitive_view, paranoid_view, - custom_exception_reporter_filter_view) + custom_exception_reporter_filter_view, sensitive_method_view) class DebugViewTests(TestCase): @@ -238,7 +237,8 @@ class ExceptionReportTestMixin(object): 'hash-brown-key': 'hash-brown-value', 'bacon-key': 'bacon-value',} - def verify_unsafe_response(self, view, check_for_vars=True): + def verify_unsafe_response(self, view, check_for_vars=True, + check_for_POST_params=True): """ Asserts that potentially sensitive info are displayed in the response. """ @@ -250,13 +250,14 @@ class ExceptionReportTestMixin(object): self.assertContains(response, 'scrambled', status_code=500) self.assertContains(response, 'sauce', status_code=500) self.assertContains(response, 'worcestershire', status_code=500) + if check_for_POST_params: + for k, v in self.breakfast_data.items(): + # All POST parameters are shown. + self.assertContains(response, k, status_code=500) + self.assertContains(response, v, status_code=500) - for k, v in self.breakfast_data.items(): - # All POST parameters are shown. - self.assertContains(response, k, status_code=500) - self.assertContains(response, v, status_code=500) - - def verify_safe_response(self, view, check_for_vars=True): + def verify_safe_response(self, view, check_for_vars=True, + check_for_POST_params=True): """ Asserts that certain sensitive info are not displayed in the response. """ @@ -269,18 +270,19 @@ class ExceptionReportTestMixin(object): # Sensitive variable's name is shown but not its value. self.assertContains(response, 'sauce', status_code=500) self.assertNotContains(response, 'worcestershire', status_code=500) + if check_for_POST_params: + for k, v in self.breakfast_data.items(): + # All POST parameters' names are shown. + self.assertContains(response, k, status_code=500) + # Non-sensitive POST parameters' values are shown. + self.assertContains(response, 'baked-beans-value', status_code=500) + self.assertContains(response, 'hash-brown-value', status_code=500) + # Sensitive POST parameters' values are not shown. + self.assertNotContains(response, 'sausage-value', status_code=500) + self.assertNotContains(response, 'bacon-value', status_code=500) - for k, v in self.breakfast_data.items(): - # All POST parameters' names are shown. - self.assertContains(response, k, status_code=500) - # Non-sensitive POST parameters' values are shown. - self.assertContains(response, 'baked-beans-value', status_code=500) - self.assertContains(response, 'hash-brown-value', status_code=500) - # Sensitive POST parameters' values are not shown. - self.assertNotContains(response, 'sausage-value', status_code=500) - self.assertNotContains(response, 'bacon-value', status_code=500) - - def verify_paranoid_response(self, view, check_for_vars=True): + def verify_paranoid_response(self, view, check_for_vars=True, + check_for_POST_params=True): """ Asserts that no variables or POST parameters are displayed in the response. """ @@ -292,14 +294,14 @@ class ExceptionReportTestMixin(object): self.assertNotContains(response, 'scrambled', status_code=500) self.assertContains(response, 'sauce', status_code=500) self.assertNotContains(response, 'worcestershire', status_code=500) + if check_for_POST_params: + for k, v in self.breakfast_data.items(): + # All POST parameters' names are shown. + self.assertContains(response, k, status_code=500) + # No POST parameters' values are shown. + self.assertNotContains(response, v, status_code=500) - for k, v in self.breakfast_data.items(): - # All POST parameters' names are shown. - self.assertContains(response, k, status_code=500) - # No POST parameters' values are shown. - self.assertNotContains(response, v, status_code=500) - - def verify_unsafe_email(self, view): + def verify_unsafe_email(self, view, check_for_POST_params=True): """ Asserts that potentially sensitive info are displayed in the email report. """ @@ -314,12 +316,13 @@ class ExceptionReportTestMixin(object): self.assertNotIn('scrambled', email.body) self.assertNotIn('sauce', email.body) self.assertNotIn('worcestershire', email.body) - for k, v in self.breakfast_data.items(): - # All POST parameters are shown. - self.assertIn(k, email.body) - self.assertIn(v, email.body) + if check_for_POST_params: + for k, v in self.breakfast_data.items(): + # All POST parameters are shown. + self.assertIn(k, email.body) + self.assertIn(v, email.body) - def verify_safe_email(self, view): + def verify_safe_email(self, view, check_for_POST_params=True): """ Asserts that certain sensitive info are not displayed in the email report. """ @@ -334,15 +337,16 @@ class ExceptionReportTestMixin(object): self.assertNotIn('scrambled', email.body) self.assertNotIn('sauce', email.body) self.assertNotIn('worcestershire', email.body) - for k, v in self.breakfast_data.items(): - # All POST parameters' names are shown. - self.assertIn(k, email.body) - # Non-sensitive POST parameters' values are shown. - self.assertIn('baked-beans-value', email.body) - self.assertIn('hash-brown-value', email.body) - # Sensitive POST parameters' values are not shown. - self.assertNotIn('sausage-value', email.body) - self.assertNotIn('bacon-value', email.body) + if check_for_POST_params: + for k, v in self.breakfast_data.items(): + # All POST parameters' names are shown. + self.assertIn(k, email.body) + # Non-sensitive POST parameters' values are shown. + self.assertIn('baked-beans-value', email.body) + self.assertIn('hash-brown-value', email.body) + # Sensitive POST parameters' values are not shown. + self.assertNotIn('sausage-value', email.body) + self.assertNotIn('bacon-value', email.body) def verify_paranoid_email(self, view): """ @@ -425,6 +429,24 @@ class ExceptionReporterFilterTests(TestCase, ExceptionReportTestMixin): self.verify_unsafe_response(custom_exception_reporter_filter_view) self.verify_unsafe_email(custom_exception_reporter_filter_view) + def test_sensitive_method(self): + """ + Ensure that the sensitive_variables decorator works with object + methods. + Refs #18379. + """ + with self.settings(DEBUG=True): + self.verify_unsafe_response(sensitive_method_view, + check_for_POST_params=False) + self.verify_unsafe_email(sensitive_method_view, + check_for_POST_params=False) + + with self.settings(DEBUG=False): + self.verify_safe_response(sensitive_method_view, + check_for_POST_params=False) + self.verify_safe_email(sensitive_method_view, + check_for_POST_params=False) + class AjaxResponseExceptionReporterFilter(TestCase, ExceptionReportTestMixin): """ diff --git a/tests/regressiontests/views/tests/static.py b/tests/regressiontests/views/tests/static.py index 6cabf6453c..9d87ade137 100644 --- a/tests/regressiontests/views/tests/static.py +++ b/tests/regressiontests/views/tests/static.py @@ -29,7 +29,7 @@ class StaticTests(TestCase): for filename in media_files: response = self.client.get('/views/%s/%s' % (self.prefix, filename)) file_path = path.join(media_dir, filename) - with open(file_path) as fp: + with open(file_path, 'rb') as fp: self.assertEqual(fp.read(), response.content) self.assertEqual(len(response.content), int(response['Content-Length'])) self.assertEqual(mimetypes.guess_type(file_path)[1], response.get('Content-Encoding', None)) @@ -41,14 +41,14 @@ class StaticTests(TestCase): def test_copes_with_empty_path_component(self): file_name = 'file.txt' response = self.client.get('/views/%s//%s' % (self.prefix, file_name)) - with open(path.join(media_dir, file_name)) as fp: + with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response.content) def test_is_modified_since(self): file_name = 'file.txt' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE='Thu, 1 Jan 1970 00:00:00 GMT') - with open(path.join(media_dir, file_name)) as fp: + with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response.content) def test_not_modified_since(self): @@ -71,7 +71,7 @@ class StaticTests(TestCase): invalid_date = 'Mon, 28 May 999999999999 28:25:26 GMT' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) - with open(path.join(media_dir, file_name)) as fp: + with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response.content) self.assertEqual(len(response.content), int(response['Content-Length'])) @@ -86,7 +86,7 @@ class StaticTests(TestCase): invalid_date = ': 1291108438, Wed, 20 Oct 2010 14:05:00 GMT' response = self.client.get('/views/%s/%s' % (self.prefix, file_name), HTTP_IF_MODIFIED_SINCE=invalid_date) - with open(path.join(media_dir, file_name)) as fp: + with open(path.join(media_dir, file_name), 'rb') as fp: self.assertEqual(fp.read(), response.content) self.assertEqual(len(response.content), int(response['Content-Length'])) diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py index 8e530cd2d8..2836d1bdde 100644 --- a/tests/regressiontests/views/views.py +++ b/tests/regressiontests/views/views.py @@ -2,7 +2,6 @@ from __future__ import absolute_import import sys -from django import forms from django.core.exceptions import PermissionDenied from django.core.urlresolvers import get_resolver from django.http import HttpResponse, HttpResponseRedirect @@ -14,7 +13,7 @@ from django.views.decorators.debug import (sensitive_post_parameters, from django.utils.log import getLogger from . import BrokenException, except_args -from .models import Article + def index_page(request): @@ -209,3 +208,23 @@ def custom_exception_reporter_filter_view(request): exc_info = sys.exc_info() send_log(request, exc_info) return technical_500_response(request, *exc_info) + + +class Klass(object): + + @sensitive_variables('sauce') + def method(self, request): + # Do not just use plain strings for the variables' values in the code + # so that the tests don't return false positives when the function's + # source is displayed in the exception report. + cooked_eggs = ''.join(['s', 'c', 'r', 'a', 'm', 'b', 'l', 'e', 'd']) + sauce = ''.join(['w', 'o', 'r', 'c', 'e', 's', 't', 'e', 'r', 's', 'h', 'i', 'r', 'e']) + try: + raise Exception + except Exception: + exc_info = sys.exc_info() + send_log(request, exc_info) + return technical_500_response(request, *exc_info) + +def sensitive_method_view(request): + return Klass().method(request) \ No newline at end of file
    + + {% blocktrans with name=app.name %}{{ name }}{% endblocktrans %} + +
    File1:
    • This field is required.
    File1:
    • The submitted file is empty.
    File1:
    • No file was submitted. Check the encoding type on the form.
    File1:
    File1: