diff --git a/AUTHORS b/AUTHORS index 973e32d05a..80f277996b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,18 +12,25 @@ The PRIMARY AUTHORS are (and/or have been): * Luke Plant * Russell Keith-Magee * Robert Wittams + * James Bennett * Gary Wilson + * Matt Boersma + * Ian Kelly + * Joseph Kocherhans * Brian Rosner * Justin Bronn * Karen Tracey * Jannis Leidel * James Tauber * Alex Gaynor + * Simon Meers * Andrew Godwin * Carl Meyer * Ramiro Morales + * Gabriel Hurley * Chris Beaven * Honza Král + * Tim Graham * Idan Gazit * Paul McMillan * Julien Phalip @@ -36,6 +43,7 @@ The PRIMARY AUTHORS are (and/or have been): * Preston Holmes * Simon Charette * Donald Stufft + * Daniel Lindsley * Marc Tamlyn More information on the main contributors to Django can be found in @@ -84,14 +92,15 @@ answer newbie questions, and generally made Django that much better: Randy Barlow <randy@electronsweatshop.com> Scott Barr <scott@divisionbyzero.com.au> Jiri Barton + Jorge Bastida <me@jorgebastida.com> Ned Batchelder <http://www.nedbatchelder.com/> batiste@dosimple.ch Batman Brian Beck <http://blog.brianbeck.com/> Shannon -jj Behrens <http://jjinux.blogspot.com/> Esdras Beleza <linux@esdrasbeleza.com> + Božidar Benko <bbenko@gmail.com> Chris Bennett <chrisrbennett@yahoo.com> - James Bennett Danilo Bargen Shai Berger <shai@platonix.com> berto @@ -102,9 +111,9 @@ answer newbie questions, and generally made Django that much better: Paul Bissex <http://e-scribe.com/> Loïc Bistuer <loic.bistuer@sixmedia.com> Simon Blanchard + Jérémie Blaser <blaserje@gmail.com> Craig Blaszczyk <masterjakul@gmail.com> David Blewett <david@dawninglight.net> - Matt Boersma <matt@sprout.org> Artem Gnilov <boobsd@gmail.com> Matías Bordese Nate Bragg <jonathan.bragg@alum.rpi.edu> @@ -117,6 +126,7 @@ answer newbie questions, and generally made Django that much better: bthomas btoll@bestweb.net Jonathan Buchanan <jonathan.buchanan@gmail.com> + Jacob Burch <jacobburch@gmail.com> Keith Bussell <kbussell@gmail.com> C8E Chris Cahoon <chris.cahoon@gmail.com> @@ -149,6 +159,7 @@ answer newbie questions, and generally made Django that much better: Paul Collins <paul.collins.iii@gmail.com> Robert Coup Deric Crago <deric.crago@gmail.com> + Brian Fabian Crain <http://www.bfc.do/> David Cramer <dcramer@gmail.com> Pete Crosier <pete.crosier@gmail.com> Matt Croydon <http://www.postneo.com/> @@ -156,6 +167,7 @@ answer newbie questions, and generally made Django that much better: Leah Culver <leah.culver@gmail.com> Raúl Cumplido <raulcumplido@gmail.com> flavio.curella@gmail.com + Tome Cvitan <tome@cvitan.com> John D'Agostino <john.dagostino@gmail.com> dackze+django@gmail.com Jim Dalton <jim.dalton@gmail.com> @@ -188,6 +200,7 @@ answer newbie questions, and generally made Django that much better: J. Clifford Dyer <jcd@sdf.lonestar.org> Clint Ecker Nick Efford <nick@efford.org> + Marc Egli <frog32@me.com> eibaan@gmail.com David Eklund Julia Elman @@ -212,6 +225,7 @@ answer newbie questions, and generally made Django that much better: Stefane Fermgier <sf@fermigier.com> J. Pablo Fernandez <pupeno@pupeno.com> Maciej Fijalkowski + Leandra Finger <leandra.finger@gmail.com> Juan Pedro Fisanotti <fisadev@gmail.com> Ben Firshman <ben@firshman.co.uk> Matthew Flanagan <http://wadofstuff.blogspot.com> @@ -239,6 +253,7 @@ answer newbie questions, and generally made Django that much better: pradeep.gowda@gmail.com Collin Grady <collin@collingrady.com> Gabriel Grant <g@briel.ca> + Martin Green Daniel Greenfeld Simon Greenhill <dev@simon.net.nz> Owen Griffiths @@ -268,6 +283,7 @@ answer newbie questions, and generally made Django that much better: Eric Holscher <http://ericholscher.com> Ian Holsman <http://feh.holsman.net/> Kieran Holland <http://www.kieranholland.com> + Markus Holtermann <http://markusholtermann.eu> Sung-Jin Hong <serialx.net@gmail.com> Leo "hylje" Honkanen <sealage@gmail.com> Matt Hoskins <skaffenuk@googlemail.com> @@ -278,7 +294,6 @@ answer newbie questions, and generally made Django that much better: Rob Hudson <http://rob.cogit8.org/> Jason Huggins <http://www.jrandolph.com/blog/> Jeff Hui <jeffkhui@gmail.com> - Gabriel Hurley <gabriel@strikeawe.com> Hyun Mi Ae Ibon <ibonso@gmail.com> Tom Insam @@ -327,12 +342,12 @@ answer newbie questions, and generally made Django that much better: Meir Kriheli <http://mksoft.co.il/> Bruce Kroeze <http://coderseye.com/> krzysiek.pawlik@silvermedia.pl - Joseph Kocherhans konrad@gwu.edu knox <christobzr@gmail.com> David Krauth Kevin Kubasik <kevin@kubasik.net> kurtiss@meetro.com + Vladimir Kuzma <vladimirkuzma.ch@gmail.com> Denis Kuzmichyov <kuzmichyov@gmail.com> Panos Laganakos <panos.laganakos@gmail.com> Nick Lane <nick.lane.au@gmail.com> @@ -360,7 +375,6 @@ answer newbie questions, and generally made Django that much better: limodou Philip Lindborg <philip.lindborg@gmail.com> Simon Litchfield <simon@quo.com.au> - Daniel Lindsley <daniel@toastdriven.com> Trey Long <trey@ktrl.com> Laurent Luce <http://www.laurentluce.com> Martin Mahner <http://www.mahner.org/> @@ -399,6 +413,7 @@ answer newbie questions, and generally made Django that much better: Slawek Mikula <slawek dot mikula at gmail dot com> Katie Miller <katie@sub50.com> Shawn Milochik <shawn@milochik.com> + Baptiste Mispelon <bmispelon@gmail.com> mitakummaa@gmail.com Taylor Mitchell <taylor.mitchell@gmail.com> mmarshall @@ -458,6 +473,7 @@ answer newbie questions, and generally made Django that much better: Jyrki Pulliainen <jyrki.pulliainen@gmail.com> Thejaswi Puthraya <thejaswi.puthraya@gmail.com> Johann Queuniet <johann.queuniet@adh.naellia.eu> + Ram Rachum <ram@rachum.com> Jan Rademaker Michael Radziej <mir@noris.de> Laurent Rahuel <laurent.rahuel@gmail.com> @@ -465,6 +481,7 @@ answer newbie questions, and generally made Django that much better: Luciano Ramalho Amit Ramon <amit.ramon@gmail.com> Philippe Raoult <philippe.raoult@n2nsoft.com> + Senko Rašić <senko.rasic@dobarkod.hr> Massimiliano Ravelli <massimiliano.ravelli@gmail.com> Brian Ray <http://brianray.chipy.org/> Lee Reilly <lee@leereilly.net> @@ -480,6 +497,7 @@ answer newbie questions, and generally made Django that much better: Alex Robbins <alexander.j.robbins@gmail.com> Matt Robenolt <m@robenolt.com> Henrique Romano <onaiort@gmail.com> + Erik Romijn <django@solidlinks.nl> Armin Ronacher Daniel Roseman <http://roseman.org.uk/> Rozza <ross.lawley@gmail.com> @@ -499,6 +517,7 @@ answer newbie questions, and generally made Django that much better: Bernd Schlapsi schwank@gmail.com scott@staplefish.com + Olivier Sels <olivier.sels@gmail.com> Ilya Semenov <semenov@inetss.com> Aleksandra Sendecka <asendecka@hauru.eu> serbaut@gmail.com @@ -523,11 +542,13 @@ answer newbie questions, and generally made Django that much better: Don Spaulding <donspauldingii@gmail.com> Calvin Spealman <ironfroggy@gmail.com> Dane Springmeyer + Silvan Spross <silvan.spross@gmail.com> Bjørn Stabell <bjorn@exoweb.net> Georgi Stanojevski <glisha@gmail.com> starrynight <cmorgh@gmail.com> Vasiliy Stavenko <stavenko@gmail.com> Thomas Steinacher <http://www.eggdrop.ch/> + Emil Stenström <em@kth.se> Johan C. Stöver <johan@nilling.nl> Nowell Strite <http://nowell.strite.org/> Thomas Stromberg <tstromberg@google.com> @@ -573,12 +594,14 @@ answer newbie questions, and generally made Django that much better: I.S. van Oostveen <v.oostveen@idca.nl> viestards.lists@gmail.com George Vilches <gav@thataddress.com> + Simeon Visser <http://simeonvisser.com> Vlado <vlado@labath.org> Zachary Voase <zacharyvoase@gmail.com> Marijn Vriens <marijn@metronomo.cl> Milton Waddams Chris Wagner <cw264701@ohio.edu> Rick Wagner <rwagner@physics.ucsd.edu> + Gavin Wahl <gavinwahl@gmail.com> wam-djangobug@wamber.net Wang Chun <wangchun@exoweb.net> Filip Wasilewski <filip.wasilewski@gmail.com> diff --git a/django/__init__.py b/django/__init__.py index 873c328add..5a1c74efa7 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1,4 +1,4 @@ -VERSION = (1, 6, 0, 'alpha', 0) +VERSION = (1, 6, 0, 'alpha', 1) def get_version(*args, **kwargs): # Don't litter django/__init__.py with all the get_version stuff. diff --git a/django/conf/__init__.py b/django/conf/__init__.py index f876c490c8..61584391cd 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -127,7 +127,10 @@ class Settings(BaseSettings): try: mod = importlib.import_module(self.SETTINGS_MODULE) except ImportError as e: - raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_MODULE, e)) + raise ImportError( + "Could not import settings '%s' (Is it on sys.path? Is there an import error in the settings file?): %s" + % (self.SETTINGS_MODULE, e) + ) # Settings that should be converted into tuples if they're mistakenly entered # as strings. diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 53aef351c0..596f4ae78a 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -131,7 +131,7 @@ LANGUAGES = ( ) # Languages using BiDi (right-to-left) layout -LANGUAGES_BIDI = ("he", "ar", "fa") +LANGUAGES_BIDI = ("he", "ar", "fa", "ur") # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. diff --git a/django/conf/locale/en/LC_MESSAGES/django.po b/django/conf/locale/en/LC_MESSAGES/django.po index 4c94d5a00a..371f0af2ab 100644 --- a/django/conf/locale/en/LC_MESSAGES/django.po +++ b/django/conf/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:17+0200\n" +"POT-Creation-Date: 2013-05-25 14:27+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English <en@li.org>\n" @@ -337,7 +337,7 @@ msgstr "" msgid "Enter a valid value." msgstr "" -#: core/validators.py:53 forms/fields.py:640 +#: core/validators.py:53 forms/fields.py:639 msgid "Enter a valid URL." msgstr "" @@ -362,7 +362,7 @@ msgstr "" msgid "Enter a valid IPv4 or IPv6 address." msgstr "" -#: core/validators.py:175 db/models/fields/__init__.py:704 +#: core/validators.py:175 db/models/fields/__init__.py:706 msgid "Enter only digits separated by commas." msgstr "" @@ -408,7 +408,7 @@ msgstr[1] "" msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "" -#: db/models/base.py:905 forms/models.py:605 +#: db/models/base.py:905 forms/models.py:643 msgid "and" msgstr "" @@ -435,156 +435,156 @@ msgstr "" msgid "Field of type: %(field_type)s" msgstr "" -#: db/models/fields/__init__.py:568 db/models/fields/__init__.py:1034 +#: db/models/fields/__init__.py:570 db/models/fields/__init__.py:1036 msgid "Integer" msgstr "" -#: db/models/fields/__init__.py:572 db/models/fields/__init__.py:1032 +#: db/models/fields/__init__.py:574 db/models/fields/__init__.py:1034 #, python-format msgid "'%s' value must be an integer." msgstr "" -#: db/models/fields/__init__.py:620 +#: db/models/fields/__init__.py:622 #, python-format msgid "'%s' value must be either True or False." msgstr "" -#: db/models/fields/__init__.py:622 +#: db/models/fields/__init__.py:624 msgid "Boolean (Either True or False)" msgstr "" -#: db/models/fields/__init__.py:671 +#: db/models/fields/__init__.py:673 #, python-format msgid "String (up to %(max_length)s)" msgstr "" -#: db/models/fields/__init__.py:699 +#: db/models/fields/__init__.py:701 msgid "Comma-separated integers" msgstr "" -#: db/models/fields/__init__.py:713 +#: db/models/fields/__init__.py:715 #, python-format msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format." msgstr "" -#: db/models/fields/__init__.py:715 db/models/fields/__init__.py:803 +#: db/models/fields/__init__.py:717 db/models/fields/__init__.py:805 #, python-format msgid "" "'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date." msgstr "" -#: db/models/fields/__init__.py:718 +#: db/models/fields/__init__.py:720 msgid "Date (without time)" msgstr "" -#: db/models/fields/__init__.py:801 +#: db/models/fields/__init__.py:803 #, python-format msgid "" "'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." "uuuuuu]][TZ] format." msgstr "" -#: db/models/fields/__init__.py:805 +#: db/models/fields/__init__.py:807 #, python-format msgid "" "'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but " "it is an invalid date/time." msgstr "" -#: db/models/fields/__init__.py:809 +#: db/models/fields/__init__.py:811 msgid "Date (with time)" msgstr "" -#: db/models/fields/__init__.py:898 +#: db/models/fields/__init__.py:900 #, python-format msgid "'%s' value must be a decimal number." msgstr "" -#: db/models/fields/__init__.py:900 +#: db/models/fields/__init__.py:902 msgid "Decimal number" msgstr "" -#: db/models/fields/__init__.py:957 +#: db/models/fields/__init__.py:959 msgid "Email address" msgstr "" -#: db/models/fields/__init__.py:976 +#: db/models/fields/__init__.py:978 msgid "File path" msgstr "" -#: db/models/fields/__init__.py:1003 +#: db/models/fields/__init__.py:1005 #, python-format msgid "'%s' value must be a float." msgstr "" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1007 msgid "Floating point number" msgstr "" -#: db/models/fields/__init__.py:1066 +#: db/models/fields/__init__.py:1068 msgid "Big (8 byte) integer" msgstr "" -#: db/models/fields/__init__.py:1080 +#: db/models/fields/__init__.py:1082 msgid "IPv4 address" msgstr "" -#: db/models/fields/__init__.py:1096 +#: db/models/fields/__init__.py:1098 msgid "IP address" msgstr "" -#: db/models/fields/__init__.py:1139 +#: db/models/fields/__init__.py:1141 #, python-format msgid "'%s' value must be either None, True or False." msgstr "" -#: db/models/fields/__init__.py:1141 +#: db/models/fields/__init__.py:1143 msgid "Boolean (Either True, False or None)" msgstr "" -#: db/models/fields/__init__.py:1190 +#: db/models/fields/__init__.py:1192 msgid "Positive integer" msgstr "" -#: db/models/fields/__init__.py:1201 +#: db/models/fields/__init__.py:1203 msgid "Positive small integer" msgstr "" -#: db/models/fields/__init__.py:1212 +#: db/models/fields/__init__.py:1214 #, python-format msgid "Slug (up to %(max_length)s)" msgstr "" -#: db/models/fields/__init__.py:1230 +#: db/models/fields/__init__.py:1232 msgid "Small integer" msgstr "" -#: db/models/fields/__init__.py:1236 +#: db/models/fields/__init__.py:1238 msgid "Text" msgstr "" -#: db/models/fields/__init__.py:1254 +#: db/models/fields/__init__.py:1256 #, python-format msgid "" "'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format." msgstr "" -#: db/models/fields/__init__.py:1256 +#: db/models/fields/__init__.py:1258 #, python-format msgid "" "'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid " "time." msgstr "" -#: db/models/fields/__init__.py:1259 +#: db/models/fields/__init__.py:1261 msgid "Time" msgstr "" -#: db/models/fields/__init__.py:1321 +#: db/models/fields/__init__.py:1323 msgid "URL" msgstr "" -#: db/models/fields/__init__.py:1338 +#: db/models/fields/__init__.py:1340 msgid "Raw binary data" msgstr "" @@ -596,55 +596,50 @@ msgstr "" msgid "Image" msgstr "" -#: db/models/fields/related.py:1133 +#: db/models/fields/related.py:1118 #, python-format msgid "Model %(model)s with pk %(pk)r does not exist." msgstr "" -#: db/models/fields/related.py:1135 +#: db/models/fields/related.py:1120 msgid "Foreign Key (type determined by related field)" msgstr "" -#: db/models/fields/related.py:1272 +#: db/models/fields/related.py:1257 msgid "One-to-one relationship" msgstr "" -#: db/models/fields/related.py:1339 +#: db/models/fields/related.py:1324 msgid "Many-to-many relationship" msgstr "" -#: db/models/fields/related.py:1366 -msgid "" -"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "" - #: forms/fields.py:56 msgid "This field is required." msgstr "" -#: forms/fields.py:225 +#: forms/fields.py:227 msgid "Enter a whole number." msgstr "" -#: forms/fields.py:266 forms/fields.py:294 +#: forms/fields.py:268 forms/fields.py:296 msgid "Enter a number." msgstr "" -#: forms/fields.py:296 +#: forms/fields.py:298 #, python-format msgid "Ensure that there are no more than %(max)s digit in total." msgid_plural "Ensure that there are no more than %(max)s digits in total." msgstr[0] "" msgstr[1] "" -#: forms/fields.py:300 +#: forms/fields.py:302 #, python-format msgid "Ensure that there are no more than %(max)s decimal place." msgid_plural "Ensure that there are no more than %(max)s decimal places." msgstr[0] "" msgstr[1] "" -#: forms/fields.py:304 +#: forms/fields.py:306 #, python-format msgid "" "Ensure that there are no more than %(max)s digit before the decimal point." @@ -653,31 +648,31 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: forms/fields.py:406 forms/fields.py:1058 +#: forms/fields.py:408 forms/fields.py:1064 msgid "Enter a valid date." msgstr "" -#: forms/fields.py:430 forms/fields.py:1059 +#: forms/fields.py:432 forms/fields.py:1065 msgid "Enter a valid time." msgstr "" -#: forms/fields.py:451 +#: forms/fields.py:454 msgid "Enter a valid date/time." msgstr "" -#: forms/fields.py:525 +#: forms/fields.py:531 msgid "No file was submitted. Check the encoding type on the form." msgstr "" -#: forms/fields.py:526 +#: forms/fields.py:532 msgid "No file was submitted." msgstr "" -#: forms/fields.py:527 +#: forms/fields.py:533 msgid "The submitted file is empty." msgstr "" -#: forms/fields.py:529 +#: forms/fields.py:535 #, python-format msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." msgid_plural "" @@ -685,22 +680,22 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: forms/fields.py:532 +#: forms/fields.py:538 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" -#: forms/fields.py:593 +#: forms/fields.py:599 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: forms/fields.py:746 forms/fields.py:824 forms/models.py:1042 +#: forms/fields.py:749 forms/fields.py:828 forms/models.py:1096 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" -#: forms/fields.py:825 forms/fields.py:928 forms/models.py:1041 +#: forms/fields.py:829 forms/fields.py:933 forms/models.py:1095 msgid "Enter a list of values." msgstr "" @@ -709,53 +704,60 @@ msgstr "" msgid "(Hidden field %(name)s) %(error)s" msgstr "" -#: forms/formsets.py:305 +#: forms/formsets.py:310 #, python-format -msgid "Please submit %s or fewer forms." -msgstr "" +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" +msgstr[1] "" -#: forms/formsets.py:331 forms/formsets.py:333 +#: forms/formsets.py:337 forms/formsets.py:339 msgid "Order" msgstr "" -#: forms/formsets.py:335 +#: forms/formsets.py:341 msgid "Delete" msgstr "" -#: forms/models.py:599 +#: forms/models.py:637 #, python-format msgid "Please correct the duplicate data for %(field)s." msgstr "" -#: forms/models.py:603 +#: forms/models.py:641 #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." msgstr "" -#: forms/models.py:609 +#: forms/models.py:647 #, python-format msgid "" "Please correct the duplicate data for %(field_name)s which must be unique " "for the %(lookup)s in %(date_field)s." msgstr "" -#: forms/models.py:617 +#: forms/models.py:655 msgid "Please correct the duplicate values below." msgstr "" -#: forms/models.py:883 +#: forms/models.py:937 msgid "The inline foreign key did not match the parent instance primary key." msgstr "" -#: forms/models.py:947 +#: forms/models.py:1001 msgid "Select a valid choice. That choice is not one of the available choices." msgstr "" -#: forms/models.py:1044 +#: forms/models.py:1098 #, python-format msgid "\"%(pk)s\" is not a valid value for a primary key." msgstr "" +#: forms/models.py:1109 +msgid "" +"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "" + #: forms/util.py:84 #, python-format msgid "" @@ -791,34 +793,34 @@ msgstr "" msgid "yes,no,maybe" msgstr "" -#: template/defaultfilters.py:813 template/defaultfilters.py:824 +#: template/defaultfilters.py:813 template/defaultfilters.py:825 #, python-format msgid "%(size)d byte" msgid_plural "%(size)d bytes" msgstr[0] "" msgstr[1] "" -#: template/defaultfilters.py:826 +#: template/defaultfilters.py:827 #, python-format msgid "%s KB" msgstr "" -#: template/defaultfilters.py:828 +#: template/defaultfilters.py:829 #, python-format msgid "%s MB" msgstr "" -#: template/defaultfilters.py:830 +#: template/defaultfilters.py:831 #, python-format msgid "%s GB" msgstr "" -#: template/defaultfilters.py:832 +#: template/defaultfilters.py:833 #, python-format msgid "%s TB" msgstr "" -#: template/defaultfilters.py:833 +#: template/defaultfilters.py:835 #, python-format msgid "%s PB" msgstr "" @@ -1119,6 +1121,16 @@ msgctxt "alt. month" msgid "December" msgstr "" +#: utils/image.py:105 +#, python-format +msgid "Neither Pillow nor PIL could be imported: %s" +msgstr "" + +#: utils/image.py:127 +#, python-format +msgid "The '_imaging' module for the PIL could not be imported: %s" +msgstr "" + #: utils/text.py:70 #, python-format msgctxt "String to return when truncating text" @@ -1130,53 +1142,53 @@ msgid "or" msgstr "" #. Translators: This string is used as a separator between list elements -#: utils/text.py:242 utils/timesince.py:54 +#: utils/text.py:242 utils/timesince.py:55 msgid ", " msgstr "" -#: utils/timesince.py:22 +#: utils/timesince.py:23 #, python-format msgid "%d year" msgid_plural "%d years" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:23 +#: utils/timesince.py:24 #, python-format msgid "%d month" msgid_plural "%d months" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:24 +#: utils/timesince.py:25 #, python-format msgid "%d week" msgid_plural "%d weeks" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:25 +#: utils/timesince.py:26 #, python-format msgid "%d day" msgid_plural "%d days" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:26 +#: utils/timesince.py:27 #, python-format msgid "%d hour" msgid_plural "%d hours" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:27 +#: utils/timesince.py:28 #, python-format msgid "%d minute" msgid_plural "%d minutes" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:43 +#: utils/timesince.py:44 msgid "0 minutes" msgstr "" diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py index 04fb1dff59..c0340c0543 100644 --- a/django/conf/urls/__init__.py +++ b/django/conf/urls/__init__.py @@ -5,8 +5,9 @@ from django.utils.importlib import import_module from django.utils import six -__all__ = ['handler403', 'handler404', 'handler500', 'include', 'patterns', 'url'] +__all__ = ['handler400', 'handler403', 'handler404', 'handler500', 'include', 'patterns', 'url'] +handler400 = 'django.views.defaults.bad_request' handler403 = 'django.views.defaults.permission_denied' handler404 = 'django.views.defaults.page_not_found' handler500 = 'django.views.defaults.server_error' diff --git a/django/contrib/admin/exceptions.py b/django/contrib/admin/exceptions.py new file mode 100644 index 0000000000..2e094c6da1 --- /dev/null +++ b/django/contrib/admin/exceptions.py @@ -0,0 +1,6 @@ +from django.core.exceptions import SuspiciousOperation + + +class DisallowedModelAdminLookup(SuspiciousOperation): + """Invalid filter was passed to admin view via URL querystring""" + pass diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index ff66d3e3f3..4131494515 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -216,7 +216,7 @@ class RelatedFieldListFilter(FieldListFilter): } FieldListFilter.register(lambda f: ( - hasattr(f, 'rel') and bool(f.rel) or + bool(f.rel) if hasattr(f, 'rel') else isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter) diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index adc2302587..320d3267a7 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -131,7 +131,7 @@ class AdminField(object): classes.append('required') if not self.is_first: classes.append('inline') - attrs = classes and {'class': ' '.join(classes)} or {} + attrs = {'class': ' '.join(classes)} if classes else {} return self.field.label_tag(contents=mark_safe(contents), attrs=attrs) def errors(self): @@ -144,7 +144,7 @@ class AdminReadonlyField(object): # {{ field.name }} must be a useful class name to identify the field. # For convenience, store other field-related data here too. if callable(field): - class_name = field.__name__ != '<lambda>' and field.__name__ or '' + class_name = field.__name__ if field.__name__ != '<lambda>' else '' else: class_name = field self.field = { diff --git a/django/contrib/admin/locale/en/LC_MESSAGES/django.po b/django/contrib/admin/locale/en/LC_MESSAGES/django.po index 964b0a509b..8994d24cbb 100644 --- a/django/contrib/admin/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/admin/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:18+0200\n" +"POT-Creation-Date: 2013-05-25 14:19+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English <en@li.org>\n" @@ -18,12 +18,12 @@ msgstr "" msgid "Successfully deleted %(count)d %(items)s." msgstr "" -#: actions.py:61 options.py:1365 +#: actions.py:61 options.py:1418 #, python-format msgid "Cannot delete %(name)s" msgstr "" -#: actions.py:63 options.py:1367 +#: actions.py:63 options.py:1420 msgid "Are you sure?" msgstr "" @@ -130,157 +130,157 @@ msgstr "" msgid "LogEntry Object" msgstr "" -#: options.py:163 options.py:192 +#: options.py:173 options.py:202 msgid "None" msgstr "" -#: options.py:710 +#: options.py:763 #, python-format msgid "Changed %s." msgstr "" -#: options.py:710 options.py:720 options.py:1514 +#: options.py:763 options.py:773 options.py:1570 msgid "and" msgstr "" -#: options.py:715 +#: options.py:768 #, python-format msgid "Added %(name)s \"%(object)s\"." msgstr "" -#: options.py:719 +#: options.py:772 #, python-format msgid "Changed %(list)s for %(name)s \"%(object)s\"." msgstr "" -#: options.py:724 +#: options.py:777 #, python-format msgid "Deleted %(name)s \"%(object)s\"." msgstr "" -#: options.py:728 +#: options.py:781 msgid "No fields changed." msgstr "" -#: options.py:831 options.py:874 +#: options.py:884 options.py:927 #, python-format msgid "" "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." msgstr "" -#: options.py:849 +#: options.py:902 #, python-format msgid "" "The %(name)s \"%(obj)s\" was added successfully. You may add another " "%(name)s below." msgstr "" -#: options.py:853 +#: options.py:906 #, python-format msgid "The %(name)s \"%(obj)s\" was added successfully." msgstr "" -#: options.py:867 +#: options.py:920 #, python-format msgid "" "The %(name)s \"%(obj)s\" was changed successfully. You may edit it again " "below." msgstr "" -#: options.py:881 +#: options.py:934 #, python-format msgid "" "The %(name)s \"%(obj)s\" was changed successfully. You may add another " "%(name)s below." msgstr "" -#: options.py:887 +#: options.py:940 #, python-format msgid "The %(name)s \"%(obj)s\" was changed successfully." msgstr "" -#: options.py:965 options.py:1225 +#: options.py:1018 options.py:1278 msgid "" "Items must be selected in order to perform actions on them. No items have " "been changed." msgstr "" -#: options.py:984 +#: options.py:1037 msgid "No action selected." msgstr "" -#: options.py:1064 +#: options.py:1117 #, python-format msgid "Add %s" msgstr "" -#: options.py:1088 options.py:1333 +#: options.py:1141 options.py:1386 #, python-format msgid "%(name)s object with primary key %(key)r does not exist." msgstr "" -#: options.py:1154 +#: options.py:1207 #, python-format msgid "Change %s" msgstr "" -#: options.py:1204 +#: options.py:1257 msgid "Database error" msgstr "" -#: options.py:1267 +#: options.py:1320 #, python-format msgid "%(count)s %(name)s was changed successfully." msgid_plural "%(count)s %(name)s were changed successfully." msgstr[0] "" msgstr[1] "" -#: options.py:1294 +#: options.py:1347 #, python-format msgid "%(total_count)s selected" msgid_plural "All %(total_count)s selected" msgstr[0] "" msgstr[1] "" -#: options.py:1299 +#: options.py:1352 #, python-format msgid "0 of %(cnt)s selected" msgstr "" -#: options.py:1350 +#: options.py:1403 #, python-format msgid "The %(name)s \"%(obj)s\" was deleted successfully." msgstr "" -#: options.py:1406 +#: options.py:1459 #, python-format msgid "Change history: %s" msgstr "" #. Translators: Model verbose name and instance representation, suitable to be an item in a list -#: options.py:1508 +#: options.py:1564 #, python-format msgid "%(class_name)s %(instance)s" msgstr "" -#: options.py:1515 +#: options.py:1571 #, python-format msgid "" "Deleting %(class_name)s %(instance)s would require deleting the following " "protected related objects: %(related_objects)s" msgstr "" -#: sites.py:324 tests.py:71 templates/admin/login.html:48 +#: sites.py:318 tests.py:71 templates/admin/login.html:48 #: templates/registration/password_reset_complete.html:19 #: views/decorators.py:24 msgid "Log in" msgstr "" -#: sites.py:392 +#: sites.py:386 msgid "Site administration" msgstr "" -#: sites.py:446 +#: sites.py:440 #, python-format msgid "%s administration" msgstr "" @@ -429,9 +429,14 @@ msgstr "" #: templates/admin/auth/user/change_password.html:27 #: templates/registration/password_change_form.html:20 msgid "Please correct the error below." -msgid_plural "Please correct the errors below." -msgstr[0] "" -msgstr[1] "" +msgstr "" + +#: templates/admin/change_form.html:44 templates/admin/change_list.html:67 +#: templates/admin/login.html:17 +#: templates/admin/auth/user/change_password.html:27 +#: templates/registration/password_change_form.html:20 +msgid "Please correct the errors below." +msgstr "" #: templates/admin/change_list.html:58 #, python-format @@ -811,16 +816,16 @@ msgstr "" msgid "All dates" msgstr "" -#: views/main.py:37 +#: views/main.py:35 msgid "(None)" msgstr "" -#: views/main.py:86 +#: views/main.py:84 #, python-format msgid "Select %s" msgstr "" -#: views/main.py:88 +#: views/main.py:86 #, python-format msgid "Select %s to change" msgstr "" diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 7373837bb0..34583ebf74 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,5 +1,6 @@ import copy -from functools import update_wrapper, partial +import operator +from functools import partial, reduce, update_wrapper from django import forms from django.conf import settings @@ -9,7 +10,8 @@ from django.forms.models import (modelform_factory, modelformset_factory, from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, - model_format_dict, NestedObjects) + model_format_dict, NestedObjects, lookup_needs_distinct) +from django.contrib.admin import validation from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect @@ -22,6 +24,7 @@ from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist from django.db.models.sql.constants import QUERY_TERMS from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http.response import HttpResponseBase from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils.decorators import method_decorator @@ -87,6 +90,14 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): readonly_fields = () ordering = None + # validation + validator_class = validation.BaseValidator + + @classmethod + def validate(cls, model): + validator = cls.validator_class() + validator.validate(cls, model) + def __init__(self): overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy() overrides.update(self.formfield_overrides) @@ -246,6 +257,34 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): """ return self.prepopulated_fields + def get_search_results(self, request, queryset, search_term): + # Apply keyword searches. + def construct_search(field_name): + if field_name.startswith('^'): + return "%s__istartswith" % field_name[1:] + elif field_name.startswith('='): + return "%s__iexact" % field_name[1:] + elif field_name.startswith('@'): + return "%s__search" % field_name[1:] + else: + return "%s__icontains" % field_name + + use_distinct = False + if self.search_fields and search_term: + orm_lookups = [construct_search(str(search_field)) + for search_field in self.search_fields] + for bit in search_term.split(): + or_queries = [models.Q(**{orm_lookup: bit}) + for orm_lookup in orm_lookups] + queryset = queryset.filter(reduce(operator.or_, or_queries)) + if not use_distinct: + for search_spec in orm_lookups: + if lookup_needs_distinct(self.opts, search_spec): + use_distinct = True + break + + return queryset, use_distinct + def get_queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the @@ -371,6 +410,9 @@ class ModelAdmin(BaseModelAdmin): actions_on_bottom = False actions_selection_counter = True + # validation + validator_class = validation.ModelAdminValidator + def __init__(self, model, admin_site): self.model = model self.opts = model._meta @@ -456,7 +498,7 @@ class ModelAdmin(BaseModelAdmin): "Hook for specifying fieldsets for the add form." if self.declared_fieldsets: return self.declared_fieldsets - form = self.get_form(request, obj) + form = self.get_form(request, obj, fields=None) fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) return [(None, {'fields': fields})] @@ -465,10 +507,10 @@ class ModelAdmin(BaseModelAdmin): Returns a Form class for use in the admin add view. This is used by add_view and change_view. """ - if self.declared_fieldsets: - fields = flatten_fieldsets(self.declared_fieldsets) + if 'fields' in kwargs: + fields = kwargs.pop('fields') else: - fields = None + fields = flatten_fieldsets(self.get_fieldsets(request, obj)) if self.exclude is None: exclude = [] else: @@ -985,10 +1027,10 @@ class ModelAdmin(BaseModelAdmin): response = func(self, request, queryset) - # Actions may return an HttpResponse, which will be used as the - # response from the POST. If not, we'll be a good little HTTP - # citizen and redirect back to the changelist page. - if isinstance(response, HttpResponse): + # Actions may return an HttpResponse-like object, which will be + # used as the response from the POST. If not, we'll be a good + # little HTTP citizen and redirect back to the changelist page. + if isinstance(response, HttpResponseBase): return response else: return HttpResponseRedirect(request.get_full_path()) @@ -1447,6 +1489,9 @@ class InlineModelAdmin(BaseModelAdmin): verbose_name_plural = None can_delete = True + # validation + validator_class = validation.InlineValidator + def __init__(self, parent_model, admin_site): self.admin_site = admin_site self.parent_model = parent_model @@ -1467,12 +1512,20 @@ class InlineModelAdmin(BaseModelAdmin): js.extend(['SelectBox.js', 'SelectFilter2.js']) return forms.Media(js=[static('admin/js/%s' % url) for url in js]) + def get_extra(self, request, obj=None, **kwargs): + """Hook for customizing the number of extra inline forms.""" + return self.extra + + def get_max_num(self, request, obj=None, **kwargs): + """Hook for customizing the max number of extra inline forms.""" + return self.max_num + def get_formset(self, request, obj=None, **kwargs): """Returns a BaseInlineFormSet class for use in admin add/change views.""" - if self.declared_fieldsets: - fields = flatten_fieldsets(self.declared_fieldsets) + if 'fields' in kwargs: + fields = kwargs.pop('fields') else: - fields = None + fields = flatten_fieldsets(self.get_fieldsets(request, obj)) if self.exclude is None: exclude = [] else: @@ -1493,8 +1546,8 @@ class InlineModelAdmin(BaseModelAdmin): "fields": fields, "exclude": exclude, "formfield_callback": partial(self.formfield_for_dbfield, request=request), - "extra": self.extra, - "max_num": self.max_num, + "extra": self.get_extra(request, obj, **kwargs), + "max_num": self.get_max_num(request, obj, **kwargs), "can_delete": can_delete, } @@ -1544,7 +1597,7 @@ class InlineModelAdmin(BaseModelAdmin): def get_fieldsets(self, request, obj=None): if self.declared_fieldsets: return self.declared_fieldsets - form = self.get_formset(request, obj).form + form = self.get_formset(request, obj, fields=None).form fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) return [(None, {'fields': fields})] diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 414d1b4f72..e0f43dfbfe 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -66,12 +66,6 @@ class AdminSite(object): if not admin_class: admin_class = ModelAdmin - # Don't import the humongous validation code unless required - if admin_class and settings.DEBUG: - from django.contrib.admin.validation import validate - else: - validate = lambda model, adminclass: None - if isinstance(model_or_iterable, ModelBase): model_or_iterable = [model_or_iterable] for model in model_or_iterable: @@ -94,8 +88,8 @@ class AdminSite(object): options['__module__'] = __name__ admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - # Validate (which might be a no-op) - validate(admin_class, model) + if admin_class is not ModelAdmin and settings.DEBUG: + admin_class.validate(model) # Instantiate the admin class to save in the registry self._registry[model] = admin_class(model, self) diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index 9d1b917b61..2a1b4d3c90 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -24,7 +24,7 @@ {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %} {% if form.errors %} <p class="errornote"> - {% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} </p> {% endif %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index daf37753dc..4accf80c46 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -41,7 +41,7 @@ {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} {% if errors %} <p class="errornote"> - {% blocktrans count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} </p> {{ adminform.form.non_field_errors }} {% endif %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index c72b6630a3..5d1a6b2714 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -64,7 +64,7 @@ {% endblock %} {% if cl.formset.errors %} <p class="errornote"> - {% blocktrans count cl.formset.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if cl.formset.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} </p> {{ cl.formset.non_form_errors }} {% endif %} diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html index 09bc971d2f..24b069cb2f 100644 --- a/django/contrib/admin/templates/admin/includes/fieldset.html +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -7,7 +7,7 @@ <div class="form-row{% if line.fields|length_is:'1' and line.errors %} errors{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}"> {% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %} {% for field in line %} - <div{% if not line.fields|length_is:'1' %} class="field-box{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}"{% endif %}> + <div{% if not line.fields|length_is:'1' %} class="field-box{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}"{% elif field.is_checkbox %} class="checkbox-row"{% endif %}> {% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %} {% if field.is_checkbox %} {{ field.field }}{{ field.label_tag }} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index 4690363891..1371514d43 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -14,7 +14,7 @@ {% block content %} {% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %} <p class="errornote"> -{% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} +{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} </p> {% endif %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index 5cb34739df..f7316a739f 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -17,7 +17,7 @@ <div> {% if form.errors %} <p class="errornote"> - {% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %} </p> {% endif %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index a220f12033..44ae5850b1 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} +{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb36=uid token=token %} {% endblock %} {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 18a45a006f..965352e0f5 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -62,7 +62,7 @@ def pagination(cl): # ON_EACH_SIDE links at either end of the "current page" link. page_range = [] if page_num > (ON_EACH_SIDE + ON_ENDS): - page_range.extend(range(0, ON_EACH_SIDE - 1)) + page_range.extend(range(0, ON_ENDS)) page_range.append(DOT) page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1)) else: diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index f70ee2731d..1b9e6aa7ef 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -53,4 +53,4 @@ def get_admin_log(parser, token): if tokens[4] != 'for_user': raise template.TemplateSyntaxError( "Fourth argument to 'get_admin_log' must be 'for_user'") - return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(len(tokens) > 5 and tokens[5] or None)) + return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(tokens[5] if len(tokens) > 5 else None)) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 97858e688e..a53cd367b4 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -37,9 +37,9 @@ def prepare_lookup_value(key, value): # if key ends with __in, split parameter into separate values if key.endswith('__in'): value = value.split(',') - # if key ends with __isnull, special case '' and false + # if key ends with __isnull, special case '' and the string literals 'false' and '0' if key.endswith('__isnull'): - if value.lower() in ('', 'false'): + if value.lower() in ('', 'false', '0'): value = False else: value = True @@ -269,8 +269,9 @@ def lookup_field(name, obj, model_admin=None): def label_for_field(name, model, model_admin=None, return_attr=False): """ - Returns a sensible label for a field name. The name can be a callable or the - name of an object attributes, as well as a genuine fields. If return_attr is + Returns a sensible label for a field name. The name can be a callable, + property (but not created with @property decorator) or the name of an + object's attribute, as well as a genuine fields. If return_attr is True, the resolved attribute (which could be a callable) is also returned. This will be None if (and only if) the name refers to a field. """ @@ -303,6 +304,10 @@ def label_for_field(name, model, model_admin=None, return_attr=False): if hasattr(attr, "short_description"): label = attr.short_description + elif (isinstance(attr, property) and + hasattr(attr, "fget") and + hasattr(attr.fget, "short_description")): + label = attr.fget.short_description elif callable(attr): if attr.__name__ == "<lambda>": label = "--" @@ -315,6 +320,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False): else: return label + def help_text_for_field(name, model): try: help_text = model._meta.get_field_by_name(name)[0].help_text diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 8d65f96cf1..222d433e53 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -3,358 +3,405 @@ from django.db import models from django.db.models.fields import FieldDoesNotExist from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model, _get_foreign_key) -from django.contrib.admin import ListFilter, FieldListFilter from django.contrib.admin.util import get_fields_from_path, NotRelationField -from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin, - ModelAdmin, HORIZONTAL, VERTICAL) + +""" +Does basic ModelAdmin option validation. Calls custom validation +classmethod in the end if it is provided in cls. The signature of the +custom validation classmethod should be: def validate(cls, model). +""" + +__all__ = ['BaseValidator', 'InlineValidator'] -__all__ = ['validate'] +class BaseValidator(object): + def __init__(self): + # Before we can introspect models, they need to be fully loaded so that + # inter-relations are set up correctly. We force that here. + models.get_apps() -def validate(cls, model): - """ - Does basic ModelAdmin option validation. Calls custom validation - classmethod in the end if it is provided in cls. The signature of the - custom validation classmethod should be: def validate(cls, model). - """ - # Before we can introspect models, they need to be fully loaded so that - # inter-relations are set up correctly. We force that here. - models.get_apps() + def validate(self, cls, model): + for m in dir(self): + if m.startswith('validate_'): + getattr(self, m)(cls, model) - opts = model._meta - validate_base(cls, model) + def check_field_spec(self, cls, model, flds, label): + """ + Validate the fields specification in `flds` from a ModelAdmin subclass + `cls` for the `model` model. Use `label` for reporting problems to the user. - # list_display - if hasattr(cls, 'list_display'): - check_isseq(cls, 'list_display', cls.list_display) - for idx, field in enumerate(cls.list_display): - if not callable(field): - if not hasattr(cls, field): - if not hasattr(model, field): - try: - opts.get_field(field) - except models.FieldDoesNotExist: - raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." - % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) - else: - # getattr(model, field) could be an X_RelatedObjectsDescriptor - f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) - if isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." - % (cls.__name__, idx, field)) - - # list_display_links - if hasattr(cls, 'list_display_links'): - check_isseq(cls, 'list_display_links', cls.list_display_links) - for idx, field in enumerate(cls.list_display_links): - if field not in cls.list_display: - raise ImproperlyConfigured("'%s.list_display_links[%d]' " - "refers to '%s' which is not defined in 'list_display'." - % (cls.__name__, idx, field)) - - # list_filter - if hasattr(cls, 'list_filter'): - check_isseq(cls, 'list_filter', cls.list_filter) - for idx, item in enumerate(cls.list_filter): - # There are three options for specifying a filter: - # 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel') - # 2: ('field', SomeFieldListFilter) - a field-based list filter class - # 3: SomeListFilter - a non-field list filter class - if callable(item) and not isinstance(item, models.Field): - # If item is option 3, it should be a ListFilter... - if not issubclass(item, ListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" - " which is not a descendant of ListFilter." - % (cls.__name__, idx, item.__name__)) - # ... but not a FieldListFilter. - if issubclass(item, FieldListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" - " which is of type FieldListFilter but is not" - " associated with a field name." - % (cls.__name__, idx, item.__name__)) - else: - if isinstance(item, (tuple, list)): - # item is option #2 - field, list_filter_class = item - if not issubclass(list_filter_class, FieldListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d][1]'" - " is '%s' which is not of type FieldListFilter." - % (cls.__name__, idx, list_filter_class.__name__)) - else: - # item is option #1 - field = item - # Validate the field string + The fields specification can be a ``fields`` option or a ``fields`` + sub-option from a ``fieldsets`` option component. + """ + for fields in flds: + # The entry in fields might be a tuple. If it is a standalone + # field, make it into a tuple to make processing easier. + if type(fields) != tuple: + fields = (fields,) + for field in fields: + if field in cls.readonly_fields: + # Stuff can be put in fields that isn't actually a + # model field if it's in readonly_fields, + # readonly_fields will handle the validation of such + # things. + continue try: - get_fields_from_path(model, field) - except (NotRelationField, FieldDoesNotExist): - raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" - " which does not refer to a Field." + f = model._meta.get_field(field) + except models.FieldDoesNotExist: + # If we can't find a field on the model that matches, it could be an + # extra field on the form; nothing to check so move on to the next field. + continue + if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: + raise ImproperlyConfigured("'%s.%s' " + "can't include the ManyToManyField field '%s' because " + "'%s' manually specifies a 'through' model." % ( + cls.__name__, label, field, field)) + + def validate_raw_id_fields(self, cls, model): + " Validate that raw_id_fields only contains field names that are listed on the model. " + if hasattr(cls, 'raw_id_fields'): + check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) + for idx, field in enumerate(cls.raw_id_fields): + f = get_field(cls, model, 'raw_id_fields', field) + if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): + raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " + "be either a ForeignKey or ManyToManyField." % (cls.__name__, idx, field)) - # list_per_page = 100 - if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int): - raise ImproperlyConfigured("'%s.list_per_page' should be a integer." - % cls.__name__) + def validate_fields(self, cls, model): + " Validate that fields only refer to existing fields, doesn't contain duplicates. " + # fields + if cls.fields: # default value is None + check_isseq(cls, 'fields', cls.fields) + self.check_field_spec(cls, model, cls.fields, 'fields') + if cls.fieldsets: + raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) + if len(cls.fields) > len(set(cls.fields)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) - # list_max_show_all - if hasattr(cls, 'list_max_show_all') and not isinstance(cls.list_max_show_all, int): - raise ImproperlyConfigured("'%s.list_max_show_all' should be an integer." - % cls.__name__) + def validate_fieldsets(self, cls, model): + " Validate that fieldsets is properly formatted and doesn't contain duplicates. " + from django.contrib.admin.options import flatten_fieldsets + if cls.fieldsets: # default value is None + check_isseq(cls, 'fieldsets', cls.fieldsets) + for idx, fieldset in enumerate(cls.fieldsets): + check_isseq(cls, 'fieldsets[%d]' % idx, fieldset) + if len(fieldset) != 2: + raise ImproperlyConfigured("'%s.fieldsets[%d]' does not " + "have exactly two elements." % (cls.__name__, idx)) + check_isdict(cls, 'fieldsets[%d][1]' % idx, fieldset[1]) + if 'fields' not in fieldset[1]: + raise ImproperlyConfigured("'fields' key is required in " + "%s.fieldsets[%d][1] field options dict." + % (cls.__name__, idx)) + self.check_field_spec(cls, model, fieldset[1]['fields'], "fieldsets[%d][1]['fields']" % idx) + flattened_fieldsets = flatten_fieldsets(cls.fieldsets) + if len(flattened_fieldsets) > len(set(flattened_fieldsets)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) - # list_editable - if hasattr(cls, 'list_editable') and cls.list_editable: - check_isseq(cls, 'list_editable', cls.list_editable) - for idx, field_name in enumerate(cls.list_editable): - try: - field = opts.get_field_by_name(field_name)[0] - except models.FieldDoesNotExist: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " - "field, '%s', not defined on %s.%s." - % (cls.__name__, idx, field_name, model._meta.app_label, model.__name__)) - if field_name not in cls.list_display: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " - "'%s' which is not defined in 'list_display'." - % (cls.__name__, idx, field_name)) - if field_name in cls.list_display_links: - raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" - " and '%s.list_display_links'" - % (field_name, cls.__name__, cls.__name__)) - if not cls.list_display_links and cls.list_display[0] in cls.list_editable: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" - " the first field in list_display, '%s', which can't be" - " used unless list_display_links is set." - % (cls.__name__, idx, cls.list_display[0])) - if not field.editable: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " - "field, '%s', which isn't editable through the admin." - % (cls.__name__, idx, field_name)) + def validate_exclude(self, cls, model): + " Validate that exclude is a sequence without duplicates. " + if cls.exclude: # default value is None + check_isseq(cls, 'exclude', cls.exclude) + if len(cls.exclude) > len(set(cls.exclude)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__) - # search_fields = () - if hasattr(cls, 'search_fields'): - check_isseq(cls, 'search_fields', cls.search_fields) + def validate_form(self, cls, model): + " Validate that form subclasses BaseModelForm. " + if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): + raise ImproperlyConfigured("%s.form does not inherit from " + "BaseModelForm." % cls.__name__) - # date_hierarchy = None - if cls.date_hierarchy: - f = get_field(cls, model, opts, 'date_hierarchy', cls.date_hierarchy) - if not isinstance(f, (models.DateField, models.DateTimeField)): - raise ImproperlyConfigured("'%s.date_hierarchy is " - "neither an instance of DateField nor DateTimeField." - % cls.__name__) + def validate_filter_vertical(self, cls, model): + " Validate that filter_vertical is a sequence of field names. " + if hasattr(cls, 'filter_vertical'): + check_isseq(cls, 'filter_vertical', cls.filter_vertical) + for idx, field in enumerate(cls.filter_vertical): + f = get_field(cls, model, 'filter_vertical', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " + "a ManyToManyField." % (cls.__name__, idx)) - # ordering = None - if cls.ordering: - check_isseq(cls, 'ordering', cls.ordering) - for idx, field in enumerate(cls.ordering): - if field == '?' and len(cls.ordering) != 1: - raise ImproperlyConfigured("'%s.ordering' has the random " - "ordering marker '?', but contains other fields as " - "well. Please either remove '?' or the other fields." + def validate_filter_horizontal(self, cls, model): + " Validate that filter_horizontal is a sequence of field names. " + if hasattr(cls, 'filter_horizontal'): + check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) + for idx, field in enumerate(cls.filter_horizontal): + f = get_field(cls, model, 'filter_horizontal', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " + "a ManyToManyField." % (cls.__name__, idx)) + + def validate_radio_fields(self, cls, model): + " Validate that radio_fields is a dictionary of choice or foreign key fields. " + from django.contrib.admin.options import HORIZONTAL, VERTICAL + if hasattr(cls, 'radio_fields'): + check_isdict(cls, 'radio_fields', cls.radio_fields) + for field, val in cls.radio_fields.items(): + f = get_field(cls, model, 'radio_fields', field) + if not (isinstance(f, models.ForeignKey) or f.choices): + raise ImproperlyConfigured("'%s.radio_fields['%s']' " + "is neither an instance of ForeignKey nor does " + "have choices set." % (cls.__name__, field)) + if not val in (HORIZONTAL, VERTICAL): + raise ImproperlyConfigured("'%s.radio_fields['%s']' " + "is neither admin.HORIZONTAL nor admin.VERTICAL." + % (cls.__name__, field)) + + def validate_prepopulated_fields(self, cls, model): + " Validate that prepopulated_fields if a dictionary containing allowed field types. " + # prepopulated_fields + if hasattr(cls, 'prepopulated_fields'): + check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) + for field, val in cls.prepopulated_fields.items(): + f = get_field(cls, model, 'prepopulated_fields', field) + if isinstance(f, (models.DateTimeField, models.ForeignKey, + models.ManyToManyField)): + raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " + "is either a DateTimeField, ForeignKey or " + "ManyToManyField. This isn't allowed." + % (cls.__name__, field)) + check_isseq(cls, "prepopulated_fields['%s']" % field, val) + for idx, f in enumerate(val): + get_field(cls, model, "prepopulated_fields['%s'][%d]" % (field, idx), f) + + def validate_ordering(self, cls, model): + " Validate that ordering refers to existing fields or is random. " + # ordering = None + if cls.ordering: + check_isseq(cls, 'ordering', cls.ordering) + for idx, field in enumerate(cls.ordering): + if field == '?' and len(cls.ordering) != 1: + raise ImproperlyConfigured("'%s.ordering' has the random " + "ordering marker '?', but contains other fields as " + "well. Please either remove '?' or the other fields." + % cls.__name__) + if field == '?': + continue + if field.startswith('-'): + field = field[1:] + # Skip ordering in the format field1__field2 (FIXME: checking + # this format would be nice, but it's a little fiddly). + if '__' in field: + continue + get_field(cls, model, 'ordering[%d]' % idx, field) + + def validate_readonly_fields(self, cls, model): + " Validate that readonly_fields refers to proper attribute or field. " + if hasattr(cls, "readonly_fields"): + check_isseq(cls, "readonly_fields", cls.readonly_fields) + for idx, field in enumerate(cls.readonly_fields): + if not callable(field): + if not hasattr(cls, field): + if not hasattr(model, field): + try: + model._meta.get_field(field) + except models.FieldDoesNotExist: + raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r." + % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) + + +class ModelAdminValidator(BaseValidator): + def validate_save_as(self, cls, model): + " Validate save_as is a boolean. " + check_type(cls, 'save_as', bool) + + def validate_save_on_top(self, cls, model): + " Validate save_on_top is a boolean. " + check_type(cls, 'save_on_top', bool) + + def validate_inlines(self, cls, model): + " Validate inline model admin classes. " + from django.contrib.admin.options import BaseModelAdmin + if hasattr(cls, 'inlines'): + check_isseq(cls, 'inlines', cls.inlines) + for idx, inline in enumerate(cls.inlines): + if not issubclass(inline, BaseModelAdmin): + raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " + "from BaseModelAdmin." % (cls.__name__, idx)) + if not inline.model: + raise ImproperlyConfigured("'model' is a required attribute " + "of '%s.inlines[%d]'." % (cls.__name__, idx)) + if not issubclass(inline.model, models.Model): + raise ImproperlyConfigured("'%s.inlines[%d].model' does not " + "inherit from models.Model." % (cls.__name__, idx)) + inline.validate(inline.model) + self.check_inline(inline, model) + + def check_inline(self, cls, parent_model): + " Validate inline class's fk field is not excluded. " + fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + if hasattr(cls, 'exclude') and cls.exclude: + if fk and fk.name in cls.exclude: + raise ImproperlyConfigured("%s cannot exclude the field " + "'%s' - this is the foreign key to the parent model " + "%s.%s." % (cls.__name__, fk.name, parent_model._meta.app_label, parent_model.__name__)) + + def validate_list_display(self, cls, model): + " Validate that list_display only contains fields or usable attributes. " + if hasattr(cls, 'list_display'): + check_isseq(cls, 'list_display', cls.list_display) + for idx, field in enumerate(cls.list_display): + if not callable(field): + if not hasattr(cls, field): + if not hasattr(model, field): + try: + model._meta.get_field(field) + except models.FieldDoesNotExist: + raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." + % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) + else: + # getattr(model, field) could be an X_RelatedObjectsDescriptor + f = fetch_attr(cls, model, "list_display[%d]" % idx, field) + if isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." + % (cls.__name__, idx, field)) + + def validate_list_display_links(self, cls, model): + " Validate that list_display_links is a unique subset of list_display. " + if hasattr(cls, 'list_display_links'): + check_isseq(cls, 'list_display_links', cls.list_display_links) + for idx, field in enumerate(cls.list_display_links): + if field not in cls.list_display: + raise ImproperlyConfigured("'%s.list_display_links[%d]' " + "refers to '%s' which is not defined in 'list_display'." + % (cls.__name__, idx, field)) + + def validate_list_filter(self, cls, model): + """ + Validate that list_filter is a sequence of one of three options: + 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel') + 2: ('field', SomeFieldListFilter) - a field-based list filter class + 3: SomeListFilter - a non-field list filter class + """ + from django.contrib.admin import ListFilter, FieldListFilter + if hasattr(cls, 'list_filter'): + check_isseq(cls, 'list_filter', cls.list_filter) + for idx, item in enumerate(cls.list_filter): + if callable(item) and not isinstance(item, models.Field): + # If item is option 3, it should be a ListFilter... + if not issubclass(item, ListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" + " which is not a descendant of ListFilter." + % (cls.__name__, idx, item.__name__)) + # ... but not a FieldListFilter. + if issubclass(item, FieldListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" + " which is of type FieldListFilter but is not" + " associated with a field name." + % (cls.__name__, idx, item.__name__)) + else: + if isinstance(item, (tuple, list)): + # item is option #2 + field, list_filter_class = item + if not issubclass(list_filter_class, FieldListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d][1]'" + " is '%s' which is not of type FieldListFilter." + % (cls.__name__, idx, list_filter_class.__name__)) + else: + # item is option #1 + field = item + # Validate the field string + try: + get_fields_from_path(model, field) + except (NotRelationField, FieldDoesNotExist): + raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" + " which does not refer to a Field." + % (cls.__name__, idx, field)) + + def validate_list_select_related(self, cls, model): + " Validate that list_select_related is a boolean, a list or a tuple. " + list_select_related = getattr(cls, 'list_select_related', None) + if list_select_related: + types = (bool, tuple, list) + if not isinstance(list_select_related, types): + raise ImproperlyConfigured("'%s.list_select_related' should be " + "either a bool, a tuple or a list" % + cls.__name__) + + def validate_list_per_page(self, cls, model): + " Validate that list_per_page is an integer. " + check_type(cls, 'list_per_page', int) + + def validate_list_max_show_all(self, cls, model): + " Validate that list_max_show_all is an integer. " + check_type(cls, 'list_max_show_all', int) + + def validate_list_editable(self, cls, model): + """ + Validate that list_editable is a sequence of editable fields from + list_display without first element. + """ + if hasattr(cls, 'list_editable') and cls.list_editable: + check_isseq(cls, 'list_editable', cls.list_editable) + for idx, field_name in enumerate(cls.list_editable): + try: + field = model._meta.get_field_by_name(field_name)[0] + except models.FieldDoesNotExist: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " + "field, '%s', not defined on %s.%s." + % (cls.__name__, idx, field_name, model._meta.app_label, model.__name__)) + if field_name not in cls.list_display: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " + "'%s' which is not defined in 'list_display'." + % (cls.__name__, idx, field_name)) + if field_name in cls.list_display_links: + raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" + " and '%s.list_display_links'" + % (field_name, cls.__name__, cls.__name__)) + if not cls.list_display_links and cls.list_display[0] in cls.list_editable: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" + " the first field in list_display, '%s', which can't be" + " used unless list_display_links is set." + % (cls.__name__, idx, cls.list_display[0])) + if not field.editable: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " + "field, '%s', which isn't editable through the admin." + % (cls.__name__, idx, field_name)) + + def validate_search_fields(self, cls, model): + " Validate search_fields is a sequence. " + if hasattr(cls, 'search_fields'): + check_isseq(cls, 'search_fields', cls.search_fields) + + def validate_date_hierarchy(self, cls, model): + " Validate that date_hierarchy refers to DateField or DateTimeField. " + if cls.date_hierarchy: + f = get_field(cls, model, 'date_hierarchy', cls.date_hierarchy) + if not isinstance(f, (models.DateField, models.DateTimeField)): + raise ImproperlyConfigured("'%s.date_hierarchy is " + "neither an instance of DateField nor DateTimeField." % cls.__name__) - if field == '?': - continue - if field.startswith('-'): - field = field[1:] - # Skip ordering in the format field1__field2 (FIXME: checking - # this format would be nice, but it's a little fiddly). - if '__' in field: - continue - get_field(cls, model, opts, 'ordering[%d]' % idx, field) - - if hasattr(cls, "readonly_fields"): - check_readonly_fields(cls, model, opts) - - # list_select_related = False - # save_as = False - # save_on_top = False - for attr in ('list_select_related', 'save_as', 'save_on_top'): - if not isinstance(getattr(cls, attr), bool): - raise ImproperlyConfigured("'%s.%s' should be a boolean." - % (cls.__name__, attr)) - # inlines = [] - if hasattr(cls, 'inlines'): - check_isseq(cls, 'inlines', cls.inlines) - for idx, inline in enumerate(cls.inlines): - if not issubclass(inline, BaseModelAdmin): - raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " - "from BaseModelAdmin." % (cls.__name__, idx)) - if not inline.model: - raise ImproperlyConfigured("'model' is a required attribute " - "of '%s.inlines[%d]'." % (cls.__name__, idx)) - if not issubclass(inline.model, models.Model): - raise ImproperlyConfigured("'%s.inlines[%d].model' does not " - "inherit from models.Model." % (cls.__name__, idx)) - validate_base(inline, inline.model) - validate_inline(inline, cls, model) +class InlineValidator(BaseValidator): + def validate_fk_name(self, cls, model): + " Validate that fk_name refers to a ForeignKey. " + if cls.fk_name: # default value is None + f = get_field(cls, model, 'fk_name', cls.fk_name) + if not isinstance(f, models.ForeignKey): + raise ImproperlyConfigured("'%s.fk_name is not an instance of " + "models.ForeignKey." % cls.__name__) -def validate_inline(cls, parent, parent_model): + def validate_extra(self, cls, model): + " Validate that extra is an integer. " + check_type(cls, 'extra', int) - # model is already verified to exist and be a Model - if cls.fk_name: # default value is None - f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name) - if not isinstance(f, models.ForeignKey): - raise ImproperlyConfigured("'%s.fk_name is not an instance of " - "models.ForeignKey." % cls.__name__) + def validate_max_num(self, cls, model): + " Validate that max_num is an integer. " + check_type(cls, 'max_num', int) - fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + def validate_formset(self, cls, model): + " Validate formset is a subclass of BaseModelFormSet. " + if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): + raise ImproperlyConfigured("'%s.formset' does not inherit from " + "BaseModelFormSet." % cls.__name__) - # extra = 3 - if not isinstance(cls.extra, int): - raise ImproperlyConfigured("'%s.extra' should be a integer." - % cls.__name__) - # max_num = None - max_num = getattr(cls, 'max_num', None) - if max_num is not None and not isinstance(max_num, int): - raise ImproperlyConfigured("'%s.max_num' should be an integer or None (default)." - % cls.__name__) - - # formset - if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): - raise ImproperlyConfigured("'%s.formset' does not inherit from " - "BaseModelFormSet." % cls.__name__) - - # exclude - if hasattr(cls, 'exclude') and cls.exclude: - if fk and fk.name in cls.exclude: - raise ImproperlyConfigured("%s cannot exclude the field " - "'%s' - this is the foreign key to the parent model " - "%s.%s." % (cls.__name__, fk.name, parent_model._meta.app_label, parent_model.__name__)) - - if hasattr(cls, "readonly_fields"): - check_readonly_fields(cls, cls.model, cls.model._meta) - -def validate_fields_spec(cls, model, opts, flds, label): - """ - Validate the fields specification in `flds` from a ModelAdmin subclass - `cls` for the `model` model. `opts` is `model`'s Meta inner class. - Use `label` for reporting problems to the user. - - The fields specification can be a ``fields`` option or a ``fields`` - sub-option from a ``fieldsets`` option component. - """ - for fields in flds: - # The entry in fields might be a tuple. If it is a standalone - # field, make it into a tuple to make processing easier. - if type(fields) != tuple: - fields = (fields,) - for field in fields: - if field in cls.readonly_fields: - # Stuff can be put in fields that isn't actually a - # model field if it's in readonly_fields, - # readonly_fields will handle the validation of such - # things. - continue - try: - f = opts.get_field(field) - except models.FieldDoesNotExist: - # If we can't find a field on the model that matches, it could be an - # extra field on the form; nothing to check so move on to the next field. - continue - if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: - raise ImproperlyConfigured("'%s.%s' " - "can't include the ManyToManyField field '%s' because " - "'%s' manually specifies a 'through' model." % ( - cls.__name__, label, field, field)) - -def validate_base(cls, model): - opts = model._meta - - # raw_id_fields - if hasattr(cls, 'raw_id_fields'): - check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) - for idx, field in enumerate(cls.raw_id_fields): - f = get_field(cls, model, opts, 'raw_id_fields', field) - if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): - raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " - "be either a ForeignKey or ManyToManyField." - % (cls.__name__, idx, field)) - - # fields - if cls.fields: # default value is None - check_isseq(cls, 'fields', cls.fields) - validate_fields_spec(cls, model, opts, cls.fields, 'fields') - if cls.fieldsets: - raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) - if len(cls.fields) > len(set(cls.fields)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) - - # fieldsets - if cls.fieldsets: # default value is None - check_isseq(cls, 'fieldsets', cls.fieldsets) - for idx, fieldset in enumerate(cls.fieldsets): - check_isseq(cls, 'fieldsets[%d]' % idx, fieldset) - if len(fieldset) != 2: - raise ImproperlyConfigured("'%s.fieldsets[%d]' does not " - "have exactly two elements." % (cls.__name__, idx)) - check_isdict(cls, 'fieldsets[%d][1]' % idx, fieldset[1]) - if 'fields' not in fieldset[1]: - raise ImproperlyConfigured("'fields' key is required in " - "%s.fieldsets[%d][1] field options dict." - % (cls.__name__, idx)) - validate_fields_spec(cls, model, opts, fieldset[1]['fields'], "fieldsets[%d][1]['fields']" % idx) - flattened_fieldsets = flatten_fieldsets(cls.fieldsets) - if len(flattened_fieldsets) > len(set(flattened_fieldsets)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) - - # exclude - if cls.exclude: # default value is None - check_isseq(cls, 'exclude', cls.exclude) - if len(cls.exclude) > len(set(cls.exclude)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__) - - # form - if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): - raise ImproperlyConfigured("%s.form does not inherit from " - "BaseModelForm." % cls.__name__) - - # filter_vertical - if hasattr(cls, 'filter_vertical'): - check_isseq(cls, 'filter_vertical', cls.filter_vertical) - for idx, field in enumerate(cls.filter_vertical): - f = get_field(cls, model, opts, 'filter_vertical', field) - if not isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " - "a ManyToManyField." % (cls.__name__, idx)) - - # filter_horizontal - if hasattr(cls, 'filter_horizontal'): - check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) - for idx, field in enumerate(cls.filter_horizontal): - f = get_field(cls, model, opts, 'filter_horizontal', field) - if not isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " - "a ManyToManyField." % (cls.__name__, idx)) - - # radio_fields - if hasattr(cls, 'radio_fields'): - check_isdict(cls, 'radio_fields', cls.radio_fields) - for field, val in cls.radio_fields.items(): - f = get_field(cls, model, opts, 'radio_fields', field) - if not (isinstance(f, models.ForeignKey) or f.choices): - raise ImproperlyConfigured("'%s.radio_fields['%s']' " - "is neither an instance of ForeignKey nor does " - "have choices set." % (cls.__name__, field)) - if not val in (HORIZONTAL, VERTICAL): - raise ImproperlyConfigured("'%s.radio_fields['%s']' " - "is neither admin.HORIZONTAL nor admin.VERTICAL." - % (cls.__name__, field)) - - # prepopulated_fields - if hasattr(cls, 'prepopulated_fields'): - check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) - for field, val in cls.prepopulated_fields.items(): - f = get_field(cls, model, opts, 'prepopulated_fields', field) - if isinstance(f, (models.DateTimeField, models.ForeignKey, - models.ManyToManyField)): - raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " - "is either a DateTimeField, ForeignKey or " - "ManyToManyField. This isn't allowed." - % (cls.__name__, field)) - check_isseq(cls, "prepopulated_fields['%s']" % field, val) - for idx, f in enumerate(val): - get_field(cls, model, opts, "prepopulated_fields['%s'][%d]" % (field, idx), f) +def check_type(cls, attr, type_): + if getattr(cls, attr, None) is not None and not isinstance(getattr(cls, attr), type_): + raise ImproperlyConfigured("'%s.%s' should be a %s." + % (cls.__name__, attr, type_.__name__ )) def check_isseq(cls, label, obj): if not isinstance(obj, (list, tuple)): @@ -364,16 +411,16 @@ def check_isdict(cls, label, obj): if not isinstance(obj, dict): raise ImproperlyConfigured("'%s.%s' must be a dictionary." % (cls.__name__, label)) -def get_field(cls, model, opts, label, field): +def get_field(cls, model, label, field): try: - return opts.get_field(field) + return model._meta.get_field(field) except models.FieldDoesNotExist: raise ImproperlyConfigured("'%s.%s' refers to field '%s' that is missing from model '%s.%s'." % (cls.__name__, label, field, model._meta.app_label, model.__name__)) -def fetch_attr(cls, model, opts, label, field): +def fetch_attr(cls, model, label, field): try: - return opts.get_field(field) + return model._meta.get_field(field) except models.FieldDoesNotExist: pass try: @@ -381,15 +428,3 @@ def fetch_attr(cls, model, opts, label, field): except AttributeError: raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s.%s'." % (cls.__name__, label, field, model._meta.app_label, model.__name__)) - -def check_readonly_fields(cls, model, opts): - check_isseq(cls, "readonly_fields", cls.readonly_fields) - for idx, field in enumerate(cls.readonly_fields): - if not callable(field): - if not hasattr(cls, field): - if not hasattr(model, field): - try: - opts.get_field(field) - except models.FieldDoesNotExist: - raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r." - % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 050d4776d0..8ea7e10fc0 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,7 +1,5 @@ -import operator import sys import warnings -from functools import reduce from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.core.paginator import InvalidPage @@ -16,6 +14,7 @@ from django.utils.translation import ugettext, ugettext_lazy from django.utils.http import urlencode from django.contrib.admin import FieldListFilter +from django.contrib.admin.exceptions import DisallowedModelAdminLookup from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.util import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -130,7 +129,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): lookup_params[force_str(key)] = value if not self.model_admin.lookup_allowed(key, value): - raise SuspiciousOperation("Filtering by %s not allowed" % key) + raise DisallowedModelAdminLookup("Filtering by %s not allowed" % key) filter_specs = [] if self.list_filter: @@ -331,7 +330,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): def get_queryset(self, request): # First, we collect all the declared list filters. (self.filter_specs, self.has_filters, remaining_lookup_params, - use_distinct) = self.get_filters(request) + filters_use_distinct) = self.get_filters(request) # Then, we let every list filter modify the queryset to its liking. qs = self.root_queryset @@ -357,56 +356,46 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): # ValueError, ValidationError, or ?. raise IncorrectLookupParameters(e) - # Use select_related() if one of the list_display options is a field - # with a relationship and the provided queryset doesn't already have - # select_related defined. if not qs.query.select_related: - if self.list_select_related: - qs = qs.select_related() - else: - for field_name in self.list_display: - try: - field = self.lookup_opts.get_field(field_name) - except models.FieldDoesNotExist: - pass - else: - if isinstance(field.rel, models.ManyToOneRel): - qs = qs.select_related() - break + qs = self.apply_select_related(qs) # Set ordering. ordering = self.get_ordering(request, qs) qs = qs.order_by(*ordering) - # Apply keyword searches. - def construct_search(field_name): - if field_name.startswith('^'): - return "%s__istartswith" % field_name[1:] - elif field_name.startswith('='): - return "%s__iexact" % field_name[1:] - elif field_name.startswith('@'): - return "%s__search" % field_name[1:] - else: - return "%s__icontains" % field_name + # Apply search results + qs, search_use_distinct = self.model_admin.get_search_results( + request, qs, self.query) - if self.search_fields and self.query: - orm_lookups = [construct_search(str(search_field)) - for search_field in self.search_fields] - for bit in self.query.split(): - or_queries = [models.Q(**{orm_lookup: bit}) - for orm_lookup in orm_lookups] - qs = qs.filter(reduce(operator.or_, or_queries)) - if not use_distinct: - for search_spec in orm_lookups: - if lookup_needs_distinct(self.lookup_opts, search_spec): - use_distinct = True - break - - if use_distinct: + # Remove duplicates from results, if necessary + if filters_use_distinct | search_use_distinct: return qs.distinct() else: return qs + def apply_select_related(self, qs): + if self.list_select_related is True: + return qs.select_related() + + if self.list_select_related is False: + if self.has_related_field_in_list_display(): + return qs.select_related() + + if self.list_select_related: + return qs.select_related(*self.list_select_related) + return qs + + def has_related_field_in_list_display(self): + for field_name in self.list_display: + try: + field = self.lookup_opts.get_field(field_name) + except models.FieldDoesNotExist: + pass + else: + if isinstance(field.rel, models.ManyToOneRel): + return True + return False + def url_for_result(self, result): pk = getattr(result, self.pk_attname) return reverse('admin:%s_%s_change' % (self.opts.app_label, diff --git a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po index 2e4dcdf13d..5a4dcd0872 100644 --- a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:18+0200\n" +"POT-Creation-Date: 2013-06-02 00:30-0400\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English <en@li.org>\n" @@ -60,12 +60,13 @@ msgstr "" msgid "number of %s" msgstr "" -#: views.py:270 +#. Translators: %s is an object type name +#: views.py:271 #, python-format -msgid "Fields on %s objects" +msgid "Attributes on %s objects" msgstr "" -#: views.py:362 +#: views.py:363 #, python-format msgid "%s does not appear to be a urlpattern object" msgstr "" @@ -83,7 +84,9 @@ msgid "Home" msgstr "" #: templates/admin_doc/bookmarklets.html:7 templates/admin_doc/index.html:7 +#: templates/admin_doc/index.html.py:10 templates/admin_doc/index.html:14 #: templates/admin_doc/missing_docutils.html:7 +#: templates/admin_doc/missing_docutils.html:14 #: templates/admin_doc/model_detail.html:15 #: templates/admin_doc/model_index.html:9 #: templates/admin_doc/template_detail.html:7 @@ -94,7 +97,7 @@ msgstr "" msgid "Documentation" msgstr "" -#: templates/admin_doc/bookmarklets.html:8 +#: templates/admin_doc/bookmarklets.html:8 templates/admin_doc/index.html:29 msgid "Bookmarklets" msgstr "" @@ -149,26 +152,202 @@ msgstr "" msgid "As above, but opens the admin page in a new window." msgstr "" -#: templates/admin_doc/model_detail.html:16 +#: templates/admin_doc/index.html:17 +#: templates/admin_doc/template_tag_index.html:9 +msgid "Tags" +msgstr "" + +#: templates/admin_doc/index.html:18 +msgid "List of all the template tags and their functions." +msgstr "" + +#: templates/admin_doc/index.html:20 +#: templates/admin_doc/template_filter_index.html:9 +msgid "Filters" +msgstr "" + +#: templates/admin_doc/index.html:21 +msgid "" +"Filters are actions which can be applied to variables in a template to alter " +"the output." +msgstr "" + +#: templates/admin_doc/index.html:23 templates/admin_doc/model_detail.html:16 #: templates/admin_doc/model_index.html:10 +#: templates/admin_doc/model_index.html:14 msgid "Models" msgstr "" +#: templates/admin_doc/index.html:24 +msgid "" +"Models are descriptions of all the objects in the system and their " +"associated fields. Each model has a list of fields which can be accessed as " +"template variables" +msgstr "" + +#: templates/admin_doc/index.html:26 templates/admin_doc/view_detail.html:8 +#: templates/admin_doc/view_index.html:9 +#: templates/admin_doc/view_index.html:12 +msgid "Views" +msgstr "" + +#: templates/admin_doc/index.html:27 +msgid "" +"Each page on the public site is generated by a view. The view defines which " +"template is used to generate the page and which objects are available to " +"that template." +msgstr "" + +#: templates/admin_doc/index.html:30 +msgid "Tools for your browser to quickly access admin functionality." +msgstr "" + +#: templates/admin_doc/missing_docutils.html:10 +msgid "Please install docutils" +msgstr "" + +#: templates/admin_doc/missing_docutils.html:17 +#, python-format +msgid "" +"The admin documentation system requires Python's <a href=\"%(link)s" +"\">docutils</a> library." +msgstr "" + +#: templates/admin_doc/missing_docutils.html:19 +#, python-format +msgid "" +"Please ask your administrators to install <a href=\"%(link)s\">docutils</a>." +msgstr "" + +#: templates/admin_doc/model_detail.html:21 +#, python-format +msgid "Model: %(name)s" +msgstr "" + +#: templates/admin_doc/model_detail.html:35 +msgid "Field" +msgstr "" + +#: templates/admin_doc/model_detail.html:36 +msgid "Type" +msgstr "" + +#: templates/admin_doc/model_detail.html:37 +msgid "Description" +msgstr "" + +#: templates/admin_doc/model_detail.html:52 +msgid "Back to Models Documentation" +msgstr "" + +#: templates/admin_doc/model_index.html:18 +msgid "Model documentation" +msgstr "" + +#: templates/admin_doc/model_index.html:43 +msgid "Model groups" +msgstr "" + #: templates/admin_doc/template_detail.html:8 msgid "Templates" msgstr "" -#: templates/admin_doc/template_filter_index.html:9 -msgid "Filters" +#: templates/admin_doc/template_detail.html:13 +#, python-format +msgid "Template: %(name)s" msgstr "" -#: templates/admin_doc/template_tag_index.html:9 -msgid "Tags" +#: templates/admin_doc/template_detail.html:16 +#, python-format +msgid "Template: \"%(name)s\"" msgstr "" -#: templates/admin_doc/view_detail.html:8 -#: templates/admin_doc/view_index.html:9 -msgid "Views" +#: templates/admin_doc/template_detail.html:20 +#, python-format +msgid "Search path for template \"%(name)s\" on %(grouper)s:" +msgstr "" + +#: templates/admin_doc/template_detail.html:23 +msgid "(does not exist)" +msgstr "" + +#: templates/admin_doc/template_detail.html:28 +msgid "Back to Documentation" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:12 +msgid "Template filters" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:16 +msgid "Template filter documentation" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:22 +#: templates/admin_doc/template_filter_index.html:43 +msgid "Built-in filters" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:23 +#, python-format +msgid "" +"To use these filters, put <code>%(code)s</code> in your template before " +"using the filter." +msgstr "" + +#: templates/admin_doc/template_tag_index.html:12 +msgid "Template tags" +msgstr "" + +#: templates/admin_doc/template_tag_index.html:16 +msgid "Template tag documentation" +msgstr "" + +#: templates/admin_doc/template_tag_index.html:22 +#: templates/admin_doc/template_tag_index.html:43 +msgid "Built-in tags" +msgstr "" + +#: templates/admin_doc/template_tag_index.html:23 +#, python-format +msgid "" +"To use these tags, put <code>%(code)s</code> in your template before using " +"the tag." +msgstr "" + +#: templates/admin_doc/view_detail.html:12 +#, python-format +msgid "View: %(name)s" +msgstr "" + +#: templates/admin_doc/view_detail.html:23 +msgid "Context:" +msgstr "" + +#: templates/admin_doc/view_detail.html:28 +msgid "Templates:" +msgstr "" + +#: templates/admin_doc/view_detail.html:32 +msgid "Back to Views Documentation" +msgstr "" + +#: templates/admin_doc/view_index.html:16 +msgid "View documentation" +msgstr "" + +#: templates/admin_doc/view_index.html:22 +msgid "Jump to site" +msgstr "" + +#: templates/admin_doc/view_index.html:35 +#, python-format +msgid "Views by URL on %(name)s" +msgstr "" + +#: templates/admin_doc/view_index.html:40 +#, python-format +msgid "View function: %(name)s" msgstr "" #: tests/test_fields.py:29 diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py new file mode 100644 index 0000000000..ee3fe2cb2f --- /dev/null +++ b/django/contrib/admindocs/middleware.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django import http + +class XViewMiddleware(object): + """ + Adds an X-View header to internal HEAD requests -- used by the documentation system. + """ + def process_view(self, request, view_func, view_args, view_kwargs): + """ + If the request method is HEAD and either the IP is internal or the + user is a logged-in staff member, quickly return with an x-header + indicating the view function. This is used by the documentation module + to lookup the view function for an arbitrary page. + """ + assert hasattr(request, 'user'), ( + "The XView middleware requires authentication middleware to be " + "installed. Edit your MIDDLEWARE_CLASSES setting to insert " + "'django.contrib.auth.middleware.AuthenticationMiddleware'.") + if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or + (request.user.is_active and request.user.is_staff)): + response = http.HttpResponse() + response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) + return response diff --git a/django/contrib/admindocs/templates/admin_doc/index.html b/django/contrib/admindocs/templates/admin_doc/index.html index 5347341dc0..2af43d78c8 100644 --- a/django/contrib/admindocs/templates/admin_doc/index.html +++ b/django/contrib/admindocs/templates/admin_doc/index.html @@ -7,27 +7,27 @@ › {% trans 'Documentation' %}</a> </div> {% endblock %} -{% block title %}Documentation{% endblock %} +{% block title %}{% trans 'Documentation' %}{% endblock %} {% block content %} -<h1>Documentation</h1> +<h1>{% trans 'Documentation' %}</h1> <div id="content-main"> - <h3><a href="tags/">Tags</a></h3> - <p>List of all the template tags and their functions.</p> + <h3><a href="tags/">{% trans 'Tags' %}</a></h3> + <p>{% trans 'List of all the template tags and their functions.' %}</p> - <h3><a href="filters/">Filters</a></h3> - <p>Filters are actions which can be applied to variables in a template to alter the output.</p> + <h3><a href="filters/">{% trans 'Filters' %}</a></h3> + <p>{% trans 'Filters are actions which can be applied to variables in a template to alter the output.' %}</p> - <h3><a href="models/">Models</a></h3> - <p>Models are descriptions of all the objects in the system and their associated fields. Each model has a list of fields which can be accessed as template variables.</p> + <h3><a href="models/">{% trans 'Models' %}</a></h3> + <p>{% trans 'Models are descriptions of all the objects in the system and their associated fields. Each model has a list of fields which can be accessed as template variables' %}.</p> - <h3><a href="views/">Views</a></h3> - <p>Each page on the public site is generated by a view. The view defines which template is used to generate the page and which objects are available to that template.</p> + <h3><a href="views/">{% trans 'Views' %}</a></h3> + <p>{% trans 'Each page on the public site is generated by a view. The view defines which template is used to generate the page and which objects are available to that template.' %}</p> - <h3><a href="bookmarklets/">Bookmarklets</a></h3> - <p>Tools for your browser to quickly access admin functionality.</p> + <h3><a href="bookmarklets/">{% trans 'Bookmarklets' %}</a></h3> + <p>{% trans 'Tools for your browser to quickly access admin functionality.' %}</p> </div> {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/missing_docutils.html b/django/contrib/admindocs/templates/admin_doc/missing_docutils.html index f8a68ce04c..bd790f779a 100644 --- a/django/contrib/admindocs/templates/admin_doc/missing_docutils.html +++ b/django/contrib/admindocs/templates/admin_doc/missing_docutils.html @@ -7,16 +7,16 @@ › {% trans 'Documentation' %}</a> </div> {% endblock %} -{% block title %}Please install docutils{% endblock %} +{% block title %}{% trans 'Please install docutils' %}{% endblock %} {% block content %} -<h1>Documentation</h1> +<h1>{% trans 'Documentation' %}</h1> <div id="content-main"> - <h3>The admin documentation system requires Python's <a href="http://docutils.sf.net/">docutils</a> library.</h3> + <h3>{% blocktrans with "http://docutils.sf.net/" as link %}The admin documentation system requires Python's <a href="{{ link }}">docutils</a> library.{% endblocktrans %}</h3> - <p>Please ask your administrators to install <a href="http://docutils.sf.net/">docutils</a>.</p> + <p>{% blocktrans with "http://docutils.sf.net/" as link %}Please ask your administrators to install <a href="{{ link }}">docutils</a>.{% endblocktrans %}</p> </div> {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/model_detail.html b/django/contrib/admindocs/templates/admin_doc/model_detail.html index 9fb4eeea14..c1e2bf1e22 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_detail.html +++ b/django/contrib/admindocs/templates/admin_doc/model_detail.html @@ -18,7 +18,7 @@ </div> {% endblock %} -{% block title %}Model: {{ name }}{% endblock %} +{% block title %}{% blocktrans %}Model: {{ name }}{% endblocktrans %}{% endblock %} {% block content %} <div id="content-main"> @@ -32,9 +32,9 @@ <table class="model"> <thead> <tr> - <th>Field</th> - <th>Type</th> - <th>Description</th> + <th>{% trans 'Field' %}</th> + <th>{% trans 'Type' %}</th> + <th>{% trans 'Description' %}</th> </tr> </thead> <tbody> @@ -49,6 +49,6 @@ </table> </div> -<p class="small"><a href="{% url 'django-admindocs-models-index' %}">‹ Back to Models Documentation</a></p> +<p class="small"><a href="{% url 'django-admindocs-models-index' %}">‹ {% trans 'Back to Models Documentation' %}</a></p> </div> {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/model_index.html b/django/contrib/admindocs/templates/admin_doc/model_index.html index 7a8c69953e..d4cde8334f 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_index.html +++ b/django/contrib/admindocs/templates/admin_doc/model_index.html @@ -11,11 +11,11 @@ </div> {% endblock %} -{% block title %}Models{% endblock %} +{% block title %}{% trans 'Models' %}{% endblock %} {% block content %} -<h1>Model documentation</h1> +<h1>{% trans 'Model documentation' %}</h1> {% regroup models by app_label as grouped_models %} @@ -40,7 +40,7 @@ {% block sidebar %} <div id="content-related" class="sidebar"> <div class="module"> -<h2>Model groups</h2> +<h2>{% trans 'Model groups' %}</h2> <ul> {% regroup models by app_label as grouped_models %} {% for group in grouped_models %} diff --git a/django/contrib/admindocs/templates/admin_doc/template_detail.html b/django/contrib/admindocs/templates/admin_doc/template_detail.html index 27fca28b4b..9535724c24 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_detail.html +++ b/django/contrib/admindocs/templates/admin_doc/template_detail.html @@ -10,20 +10,20 @@ </div> {% endblock %} -{% block title %}Template: {{ name }}{% endblock %} +{% block title %}{% blocktrans %}Template: {{ name }}{% endblocktrans %}{% endblock %} {% block content %} -<h1>Template: "{{ name }}"</h1> +<h1>{% blocktrans %}Template: "{{ name }}"{% endblocktrans %}</h1> {% regroup templates|dictsort:"site_id" by site as templates_by_site %} {% for group in templates_by_site %} - <h2>Search path for template "{{ name }}" on {{ group.grouper }}:</h2> + <h2>{% blocktrans with group.grouper as grouper %}Search path for template "{{ name }}" on {{ grouper }}:{% endblocktrans %}</h2> <ol> {% for template in group.list|dictsort:"order" %} - <li><code>{{ template.file }}</code>{% if not template.exists %} <em>(does not exist)</em>{% endif %}</li> + <li><code>{{ template.file }}</code>{% if not template.exists %} <em>{% trans '(does not exist)' %}</em>{% endif %}</li> {% endfor %} </ol> {% endfor %} -<p class="small"><a href="{% url 'django-admindocs-docroot' %}">‹ Back to Documentation</a></p> +<p class="small"><a href="{% url 'django-admindocs-docroot' %}">‹ {% trans 'Back to Documentation' %}</a></p> {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/template_filter_index.html b/django/contrib/admindocs/templates/admin_doc/template_filter_index.html index 687ebbcfec..04aac39105 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_filter_index.html +++ b/django/contrib/admindocs/templates/admin_doc/template_filter_index.html @@ -9,18 +9,18 @@ › {% trans 'Filters' %} </div> {% endblock %} -{% block title %}Template filters{% endblock %} +{% block title %}{% trans 'Template filters' %}{% endblock %} {% block content %} -<h1>Template filter documentation</h1> +<h1>{% trans 'Template filter documentation' %}</h1> <div id="content-main"> {% regroup filters|dictsort:"library" by library as filter_libraries %} {% for library in filter_libraries %} <div class="module"> - <h2>{% firstof library.grouper "Built-in filters" %}</h2> - {% if library.grouper %}<p class="small quiet">To use these filters, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the filter.</p><hr />{% endif %} + <h2>{% firstof library.grouper _("Built-in filters") %}</h2> + {% if library.grouper %}<p class="small quiet">{% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these filters, put <code>{{ code }}</code> in your template before using the filter.{% endblocktrans %}</p><hr />{% endif %} {% for filter in library.list|dictsort:"name" %} <h3 id="{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</h3> {{ filter.title }} @@ -40,7 +40,7 @@ {% regroup filters|dictsort:"library" by library as filter_libraries %} {% for library in filter_libraries %} <div class="module"> - <h2>{% firstof library.grouper "Built-in filters" %}</h2> + <h2>{% firstof library.grouper _("Built-in filters") %}</h2> <ul> {% for filter in library.list|dictsort:"name" %} <li><a href="#{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</a></li> diff --git a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html index c0fb243a99..a3c6eaadf4 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html +++ b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html @@ -9,18 +9,18 @@ › {% trans 'Tags' %} </div> {% endblock %} -{% block title %}Template tags{% endblock %} +{% block title %}{% trans 'Template tags' %}{% endblock %} {% block content %} -<h1>Template tag documentation</h1> +<h1>{% trans 'Template tag documentation' %}</h1> <div id="content-main"> {% regroup tags|dictsort:"library" by library as tag_libraries %} {% for library in tag_libraries %} <div class="module"> - <h2>{% firstof library.grouper "Built-in tags" %}</h2> - {% if library.grouper %}<p class="small quiet">To use these tags, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the tag.</p><hr />{% endif %} + <h2>{% firstof library.grouper _("Built-in tags") %}</h2> + {% if library.grouper %}<p class="small quiet">{% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these tags, put <code>{{ code }}</code> in your template before using the tag.{% endblocktrans %}</p><hr />{% endif %} {% for tag in library.list|dictsort:"name" %} <h3 id="{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</h3> <h4>{{ tag.title|striptags }}</h4> @@ -40,7 +40,7 @@ {% regroup tags|dictsort:"library" by library as tag_libraries %} {% for library in tag_libraries %} <div class="module"> - <h2>{% firstof library.grouper "Built-in tags" %}</h2> + <h2>{% firstof library.grouper _("Built-in tags") %}</h2> <ul> {% for tag in library.list|dictsort:"name" %} <li><a href="#{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</a></li> diff --git a/django/contrib/admindocs/templates/admin_doc/view_detail.html b/django/contrib/admindocs/templates/admin_doc/view_detail.html index efe5fed9ed..050e6c800b 100644 --- a/django/contrib/admindocs/templates/admin_doc/view_detail.html +++ b/django/contrib/admindocs/templates/admin_doc/view_detail.html @@ -9,7 +9,7 @@ › {{ name }} </div> {% endblock %} -{% block title %}View: {{ name }}{% endblock %} +{% block title %}{% blocktrans %}View: {{ name }}{% endblocktrans %}{% endblock %} {% block content %} @@ -20,14 +20,14 @@ {{ body }} {% if meta.Context %} -<h3>Context:</h3> +<h3>{% trans 'Context:' %}</h3> <p>{{ meta.Context }}</p> {% endif %} {% if meta.Templates %} -<h3>Templates:</h3> +<h3>{% trans 'Templates:' %}</h3> <p>{{ meta.Templates }}</p> {% endif %} -<p class="small"><a href="{% url 'django-admindocs-views-index' %}">‹ Back to Views Documentation</a></p> +<p class="small"><a href="{% url 'django-admindocs-views-index' %}">‹ {% trans 'Back to Views Documentation' %}</a></p> {% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/view_index.html b/django/contrib/admindocs/templates/admin_doc/view_index.html index 86342c6dd4..891eee7eec 100644 --- a/django/contrib/admindocs/templates/admin_doc/view_index.html +++ b/django/contrib/admindocs/templates/admin_doc/view_index.html @@ -9,17 +9,17 @@ › {% trans 'Views' %} </div> {% endblock %} -{% block title %}Views{% endblock %} +{% block title %}{% trans 'Views' %}{% endblock %} {% block content %} -<h1>View documentation</h1> +<h1>{% trans 'View documentation' %}</h1> {% regroup views|dictsort:"site_id" by site as views_by_site %} <div id="content-related" class="sidebar"> <div class="module"> -<h2>Jump to site</h2> +<h2>{% trans 'Jump to site' %}</h2> <ul> {% for site_views in views_by_site %} <li><a href="#site{{ site_views.grouper.id }}">{{ site_views.grouper.name }}</a></li> @@ -32,12 +32,12 @@ {% for site_views in views_by_site %} <div class="module"> -<h2 id="site{{ site_views.grouper.id }}">Views by URL on {{ site_views.grouper.name }}</h2> +<h2 id="site{{ site_views.grouper.id }}">{% blocktrans with site_views.grouper.name as name %}Views by URL on {{ name }}{% endblocktrans %}</h2> {% for view in site_views.list|dictsort:"url" %} {% ifchanged %} <h3><a href="{% url 'django-admindocs-views-detail' view=view.full_name %}">{{ view.url }}</a></h3> -<p class="small quiet">View function: {{ view.full_name }}</p> +<p class="small quiet">{% blocktrans with view.full_name as name %}View function: {{ name }}{% endblocktrans %}</p> <p>{{ view.title }}</p> <hr /> {% endifchanged %} @@ -46,5 +46,3 @@ {% endfor %} </div> {% endblock %} - - diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index 348727eb21..c03883def7 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -39,7 +39,7 @@ def bookmarklets(request): admin_root = urlresolvers.reverse('admin:index') return render_to_response('admin_doc/bookmarklets.html', { 'root_path': admin_root, - 'admin_url': "%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root), + 'admin_url': "%s://%s%s" % ('https' if request.is_secure() else 'http', request.get_host(), admin_root), }, context_instance=RequestContext(request)) @staff_member_required @@ -267,6 +267,7 @@ def model_detail(request, app_label, model_name): return render_to_response('admin_doc/model_detail.html', { 'root_path': urlresolvers.reverse('admin:index'), 'name': '%s.%s' % (opts.app_label, opts.object_name), + # Translators: %s is an object type name 'summary': _("Attributes on %s objects") % opts.object_name, 'description': model.__doc__, 'fields': fields, @@ -286,7 +287,7 @@ def template_detail(request, template): templates.append({ 'file': template_file, 'exists': os.path.exists(template_file), - 'contents': lambda: os.path.exists(template_file) and open(template_file).read() or '', + 'contents': lambda: open(template_file).read() if os.path.exists(template_file) else '', 'site_id': settings_mod.SITE_ID, 'site': site_obj, 'order': list(settings_mod.TEMPLATE_DIRS).index(dir), diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index ef9066657d..029193d582 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -1,8 +1,11 @@ import re -from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed +from django.conf import settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.utils.module_loading import import_by_path +from django.middleware.csrf import rotate_token + +from .signals import user_logged_in, user_logged_out, user_login_failed SESSION_KEY = '_auth_user_id' BACKEND_SESSION_KEY = '_auth_user_backend' @@ -14,7 +17,6 @@ def load_backend(path): def get_backends(): - from django.conf import settings backends = [] for backend_path in settings.AUTHENTICATION_BACKENDS: backends.append(load_backend(backend_path)) @@ -83,6 +85,7 @@ def login(request, user): request.session[BACKEND_SESSION_KEY] = user.backend if hasattr(request, 'user'): request.user = user + rotate_token(request) user_logged_in.send(sender=user.__class__, request=request, user=user) @@ -106,7 +109,6 @@ def logout(request): def get_user_model(): "Return the User model that is active in this project" - from django.conf import settings from django.db.models import get_model try: @@ -120,12 +122,13 @@ def get_user_model(): def get_user(request): - from django.contrib.auth.models import AnonymousUser + from .models import AnonymousUser try: user_id = request.session[SESSION_KEY] backend_path = request.session[BACKEND_SESSION_KEY] + assert backend_path in settings.AUTHENTICATION_BACKENDS backend = load_backend(backend_path) user = backend.get_user(user_id) or AnonymousUser() - except KeyError: + except (KeyError, AssertionError): user = AnonymousUser() return user diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 0e08d8ef31..edf2727b07 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -237,7 +237,7 @@ class PasswordResetForm(forms.Form): 'uid': int_to_base36(user.pk), 'user': user, 'token': token_generator.make_token(user), - 'protocol': use_https and 'https' or 'http', + 'protocol': 'https' if use_https else 'http', } subject = loader.render_to_string(subject_template_name, c) # Email subject *must not* contain newlines diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 685e50d498..fdf822ff74 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -80,7 +80,7 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw for perm in _get_all_permissions(klass._meta, ctype): searched_perms.append((ctype, perm)) - # Find all the Permissions that have a context_type for a model we're + # Find all the Permissions that have a content_type for a model we're # looking for. We don't need to check for codenames since we already have # a list of the ones we're going to create. all_perms = set(auth_app.Permission.objects.using(db).filter( diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 5709d25d7f..798cc805a0 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -177,7 +177,7 @@ class UserManager(BaseUserManager): now = timezone.now() if not username: raise ValueError('The given username must be set') - email = UserManager.normalize_email(email) + email = self.normalize_email(email) user = self.model(username=username, email=email, is_staff=False, is_active=True, is_superuser=False, last_login=now, date_joined=now, **extra_fields) diff --git a/django/contrib/auth/tests/test_auth_backends.py b/django/contrib/auth/tests/test_auth_backends.py index bb97c54a11..fc5a80e8dd 100644 --- a/django/contrib/auth/tests/test_auth_backends.py +++ b/django/contrib/auth/tests/test_auth_backends.py @@ -2,12 +2,14 @@ from __future__ import unicode_literals from datetime import date from django.conf import settings +from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, Group, Permission, AnonymousUser from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.auth.tests.test_custom_user import ExtensionUser, CustomPermissionsUser, CustomUser from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, get_user +from django.http import HttpRequest from django.test import TestCase from django.test.utils import override_settings @@ -402,3 +404,52 @@ class PermissionDeniedBackendTest(TestCase): settings.AUTHENTICATION_BACKENDS) + (backend, )) def test_authenticates(self): self.assertEqual(authenticate(username='test', password='test'), self.user1) + + +class NewModelBackend(ModelBackend): + pass + + +@skipIfCustomUser +class ChangedBackendSettingsTest(TestCase): + """ + Tests for changes in the settings.AUTHENTICATION_BACKENDS + """ + backend = 'django.contrib.auth.tests.test_auth_backends.NewModelBackend' + + TEST_USERNAME = 'test_user' + TEST_PASSWORD = 'test_password' + TEST_EMAIL = 'test@example.com' + + def setUp(self): + User.objects.create_user(self.TEST_USERNAME, + self.TEST_EMAIL, + self.TEST_PASSWORD) + + @override_settings(AUTHENTICATION_BACKENDS=(backend, )) + def test_changed_backend_settings(self): + """ + Tests that removing a backend configured in AUTHENTICATION_BACKENDS + make already logged-in users disconnect. + """ + + # Get a session for the test user + self.assertTrue(self.client.login( + username=self.TEST_USERNAME, + password=self.TEST_PASSWORD) + ) + + # Prepare a request object + request = HttpRequest() + request.session = self.client.session + + # Remove NewModelBackend + with self.settings(AUTHENTICATION_BACKENDS=( + 'django.contrib.auth.backends.ModelBackend',)): + # Get the user from the request + user = get_user(request) + + # Assert that the user retrieval is successful and the user is + # anonymous as the backend is not longer available. + self.assertIsNotNone(user) + self.assertTrue(user.is_anonymous()) diff --git a/django/contrib/auth/tests/test_custom_user.py b/django/contrib/auth/tests/test_custom_user.py index 0d324f0953..a3a159880a 100644 --- a/django/contrib/auth/tests/test_custom_user.py +++ b/django/contrib/auth/tests/test_custom_user.py @@ -21,7 +21,7 @@ class CustomUserManager(BaseUserManager): raise ValueError('Users must have an email address') user = self.model( - email=CustomUserManager.normalize_email(email), + email=self.normalize_email(email), date_of_birth=date_of_birth, ) diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index 04fd4941ab..fee0a29e7b 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -7,6 +7,7 @@ from django.contrib.auth.management.commands import changepassword from django.contrib.auth.models import User from django.contrib.auth.tests.test_custom_user import CustomUser from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.contenttypes.models import ContentType from django.core.management import call_command from django.core.management.base import CommandError from django.core.management.validation import get_validation_errors @@ -195,6 +196,7 @@ class PermissionDuplicationTestCase(TestCase): def tearDown(self): models.Permission._meta.permissions = self._original_permissions + ContentType.objects.clear_cache() def test_duplicated_permissions(self): """ diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index 7cbf72327e..ef305ac8f1 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -1,25 +1,31 @@ import itertools import os import re +try: + from urllib.parse import urlparse, ParseResult +except ImportError: # Python 2 + from urlparse import urlparse, ParseResult from django.conf import global_settings, settings from django.contrib.sites.models import Site, RequestSite from django.contrib.auth.models import User from django.core import mail -from django.core.exceptions import SuspiciousOperation from django.core.urlresolvers import reverse, NoReverseMatch -from django.http import QueryDict +from django.http import QueryDict, HttpRequest from django.utils.encoding import force_text from django.utils.html import escape from django.utils.http import urlquote from django.utils._os import upath from django.test import TestCase -from django.test.utils import override_settings +from django.test.utils import override_settings, patch_logger +from django.middleware.csrf import CsrfViewMiddleware +from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm, SetPasswordForm, PasswordResetForm) from django.contrib.auth.tests.utils import skipIfCustomUser +from django.contrib.auth.views import login as login_view @override_settings( @@ -46,15 +52,31 @@ class AuthViewsTestCase(TestCase): 'username': 'testclient', 'password': password, }) - self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith(settings.LOGIN_REDIRECT_URL)) self.assertTrue(SESSION_KEY in self.client.session) + return response def assertFormError(self, response, error): """Assert that error is found in response.context['form'] errors""" form_errors = list(itertools.chain(*response.context['form'].errors.values())) self.assertIn(force_text(error), form_errors) + def assertURLEqual(self, url, expected, parse_qs=False): + """ + Given two URLs, make sure all their components (the ones given by + urlparse) are equal, only comparing components that are present in both + URLs. + If `parse_qs` is True, then the querystrings are parsed with QueryDict. + This is useful if you don't want the order of parameters to matter. + Otherwise, the query strings are compared as-is. + """ + fields = ParseResult._fields + + for attr, x, y in zip(fields, urlparse(url), urlparse(expected)): + if parse_qs and attr == 'query': + x, y = QueryDict(x), QueryDict(y) + if x and y and x != y: + self.fail("%r != %r (%s doesn't match)" % (url, expected, attr)) + @skipIfCustomUser class AuthViewNamedURLTests(AuthViewsTestCase): @@ -132,28 +154,32 @@ class PasswordResetTest(AuthViewsTestCase): # produce a meaningful reset URL, we need to be certain that the # HTTP_HOST header isn't poisoned. This is done as a check when get_host() # is invoked, but we check here as a practical consequence. - with self.assertRaises(SuspiciousOperation): - self.client.post('/password_reset/', - {'email': 'staffmember@example.com'}, - HTTP_HOST='www.example:dr.frankenstein@evil.tld' - ) - self.assertEqual(len(mail.outbox), 0) + with patch_logger('django.security.DisallowedHost', 'error') as logger_calls: + response = self.client.post('/password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(mail.outbox), 0) + self.assertEqual(len(logger_calls), 1) # Skip any 500 handler action (like sending more mail...) @override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True) def test_poisoned_http_host_admin_site(self): "Poisoned HTTP_HOST headers can't be used for reset emails on admin views" - with self.assertRaises(SuspiciousOperation): - self.client.post('/admin_password_reset/', - {'email': 'staffmember@example.com'}, - HTTP_HOST='www.example:dr.frankenstein@evil.tld' - ) - self.assertEqual(len(mail.outbox), 0) + with patch_logger('django.security.DisallowedHost', 'error') as logger_calls: + response = self.client.post('/admin_password_reset/', + {'email': 'staffmember@example.com'}, + HTTP_HOST='www.example:dr.frankenstein@evil.tld' + ) + self.assertEqual(response.status_code, 400) + self.assertEqual(len(mail.outbox), 0) + self.assertEqual(len(logger_calls), 1) + def _test_confirm_start(self): # Start by creating the email response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) - self.assertEqual(response.status_code, 302) self.assertEqual(len(mail.outbox), 1) return self._read_signup_email(mail.outbox[0]) @@ -205,8 +231,6 @@ class PasswordResetTest(AuthViewsTestCase): url, path = self._test_confirm_start() response = self.client.post(path, {'new_password1': 'anewpassword', 'new_password2': 'anewpassword'}) - # It redirects us to a 'complete' page: - self.assertEqual(response.status_code, 302) # Check the password has been changed u = User.objects.get(email='staffmember@example.com') self.assertTrue(u.check_password("anewpassword")) @@ -221,6 +245,47 @@ class PasswordResetTest(AuthViewsTestCase): 'new_password2': 'x'}) self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch']) + def test_reset_redirect_default(self): + response = self.client.post('/password_reset/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/done/') + + def test_reset_custom_redirect(self): + response = self.client.post('/password_reset/custom_redirect/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_reset_custom_redirect_named(self): + response = self.client.post('/password_reset/custom_redirect/named/', + {'email': 'staffmember@example.com'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + + def test_confirm_redirect_default(self): + url, path = self._test_confirm_start() + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/reset/done/') + + def test_confirm_redirect_custom(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/') + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_confirm_redirect_custom_named(self): + url, path = self._test_confirm_start() + path = path.replace('/reset/', '/reset/custom/named/') + response = self.client.post(path, {'new_password1': 'anewpassword', + 'new_password2': 'anewpassword'}) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') + @override_settings(AUTH_USER_MODEL='auth.CustomUser') class CustomUserPasswordResetTest(AuthViewsTestCase): @@ -285,8 +350,6 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password1': 'password1', 'new_password2': 'password1', }) - self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/password_change/done/')) self.fail_login() self.login(password='password1') @@ -298,20 +361,50 @@ class ChangePasswordTest(AuthViewsTestCase): 'new_password2': 'password1', }) self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/password_change/done/')) + self.assertURLEqual(response.url, '/password_change/done/') + @override_settings(LOGIN_URL='/login/') def test_password_change_done_fails(self): - with self.settings(LOGIN_URL='/login/'): - response = self.client.get('/password_change/done/') - self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/login/?next=/password_change/done/')) + response = self.client.get('/password_change/done/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/login/?next=/password_change/done/') + + def test_password_change_redirect_default(self): + self.login() + response = self.client.post('/password_change/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_change/done/') + + def test_password_change_redirect_custom(self): + self.login() + response = self.client.post('/password_change/custom/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/custom/') + + def test_password_change_redirect_custom_named(self): + self.login() + response = self.client.post('/password_change/custom/named/', { + 'old_password': 'password', + 'new_password1': 'password1', + 'new_password2': 'password1', + }) + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') @skipIfCustomUser class LoginTest(AuthViewsTestCase): def test_current_site_in_context_after_login(self): - response = self.client.get(reverse('django.contrib.auth.views.login')) + response = self.client.get(reverse('login')) self.assertEqual(response.status_code, 200) if Site._meta.installed: site = Site.objects.get_current() @@ -323,7 +416,7 @@ class LoginTest(AuthViewsTestCase): 'Login form is not an AuthenticationForm') def test_security_check(self, password='password'): - login_url = reverse('django.contrib.auth.views.login') + login_url = reverse('login') # Those URLs should not pass the security check for bad_url in ('http://example.com', @@ -374,63 +467,103 @@ class LoginTest(AuthViewsTestCase): # the custom authentication form used by this login asserts # that a request is passed to the form successfully. + def test_login_csrf_rotate(self, password='password'): + """ + Makes sure that a login rotates the currently-used CSRF token. + """ + # Do a GET to establish a CSRF token + # TestClient isn't used here as we're testing middleware, essentially. + req = HttpRequest() + CsrfViewMiddleware().process_view(req, login_view, (), {}) + req.META["CSRF_COOKIE_USED"] = True + resp = login_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None) + token1 = csrf_cookie.coded_value + + # Prepare the POST request + req = HttpRequest() + req.COOKIES[settings.CSRF_COOKIE_NAME] = token1 + req.method = "POST" + req.POST = {'username': 'testclient', 'password': password, 'csrfmiddlewaretoken': token1} + req.REQUEST = req.POST + + # Use POST request to log in + SessionMiddleware().process_request(req) + CsrfViewMiddleware().process_view(req, login_view, (), {}) + req.META["SERVER_NAME"] = "testserver" # Required to have redirect work in login view + req.META["SERVER_PORT"] = 80 + req.META["CSRF_COOKIE_USED"] = True + resp = login_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None) + token2 = csrf_cookie.coded_value + + # Check the CSRF token switched + self.assertNotEqual(token1, token2) + + @skipIfCustomUser class LoginURLSettings(AuthViewsTestCase): - - def setUp(self): - super(LoginURLSettings, self).setUp() - self.old_LOGIN_URL = settings.LOGIN_URL - - def tearDown(self): - super(LoginURLSettings, self).tearDown() - settings.LOGIN_URL = self.old_LOGIN_URL - - def get_login_required_url(self, login_url): - settings.LOGIN_URL = login_url + """Tests for settings.LOGIN_URL.""" + def assertLoginURLEquals(self, url, parse_qs=False): response = self.client.get('/login_required/') self.assertEqual(response.status_code, 302) - return response.url + self.assertURLEqual(response.url, url, parse_qs=parse_qs) + @override_settings(LOGIN_URL='/login/') def test_standard_login_url(self): - login_url = '/login/' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = '/login_required/' - self.assertEqual(login_required_url, 'http://testserver%s?%s' % - (login_url, querystring.urlencode('/'))) + self.assertLoginURLEquals('/login/?next=/login_required/') + @override_settings(LOGIN_URL='login') + def test_named_login_url(self): + self.assertLoginURLEquals('/login/?next=/login_required/') + + @override_settings(LOGIN_URL='http://remote.example.com/login') def test_remote_login_url(self): - login_url = 'http://remote.example.com/login' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = 'http://testserver/login_required/' - self.assertEqual(login_required_url, - '%s?%s' % (login_url, querystring.urlencode('/'))) + quoted_next = urlquote('http://testserver/login_required/') + expected = 'http://remote.example.com/login?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + @override_settings(LOGIN_URL='https:///login/') def test_https_login_url(self): - login_url = 'https:///login/' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = 'http://testserver/login_required/' - self.assertEqual(login_required_url, - '%s?%s' % (login_url, querystring.urlencode('/'))) + quoted_next = urlquote('http://testserver/login_required/') + expected = 'https:///login/?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + @override_settings(LOGIN_URL='/login/?pretty=1') def test_login_url_with_querystring(self): - login_url = '/login/?pretty=1' - login_required_url = self.get_login_required_url(login_url) - querystring = QueryDict('pretty=1', mutable=True) - querystring['next'] = '/login_required/' - self.assertEqual(login_required_url, 'http://testserver/login/?%s' % - querystring.urlencode('/')) + self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/', parse_qs=True) + @override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/') def test_remote_login_url_with_next_querystring(self): - login_url = 'http://remote.example.com/login/' - login_required_url = self.get_login_required_url('%s?next=/default/' % - login_url) - querystring = QueryDict('', mutable=True) - querystring['next'] = 'http://testserver/login_required/' - self.assertEqual(login_required_url, '%s?%s' % (login_url, - querystring.urlencode('/'))) + quoted_next = urlquote('http://testserver/login_required/') + expected = 'http://remote.example.com/login/?next=%s' % quoted_next + self.assertLoginURLEquals(expected) + + +@skipIfCustomUser +class LoginRedirectUrlTest(AuthViewsTestCase): + """Tests for settings.LOGIN_REDIRECT_URL.""" + def assertLoginRedirectURLEqual(self, url): + response = self.login() + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, url) + + def test_default(self): + self.assertLoginRedirectURLEqual('/accounts/profile/') + + @override_settings(LOGIN_REDIRECT_URL='/custom/') + def test_custom(self): + self.assertLoginRedirectURLEqual('/custom/') + + @override_settings(LOGIN_REDIRECT_URL='password_reset') + def test_named(self): + self.assertLoginRedirectURLEqual('/password_reset/') + + @override_settings(LOGIN_REDIRECT_URL='http://remote.example.com/welcome/') + def test_remote(self): + self.assertLoginRedirectURLEqual('http://remote.example.com/welcome/') @skipIfCustomUser @@ -457,11 +590,11 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/next_page/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/somewhere/')) + self.assertURLEqual(response.url, '/somewhere/') response = self.client.get('/logout/next_page/?next=/login/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/login/')) + self.assertURLEqual(response.url, '/login/') self.confirm_logged_out() @@ -470,7 +603,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/next_page/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/somewhere/')) + self.assertURLEqual(response.url, '/somewhere/') self.confirm_logged_out() def test_logout_with_redirect_argument(self): @@ -478,7 +611,7 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/?next=/login/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/login/')) + self.assertURLEqual(response.url, '/login/') self.confirm_logged_out() def test_logout_with_custom_redirect_argument(self): @@ -486,11 +619,19 @@ class LogoutTest(AuthViewsTestCase): self.login() response = self.client.get('/logout/custom_query/?follow=/somewhere/') self.assertEqual(response.status_code, 302) - self.assertTrue(response.url.endswith('/somewhere/')) + self.assertURLEqual(response.url, '/somewhere/') + self.confirm_logged_out() + + def test_logout_with_named_redirect(self): + "Logout resolves names or URLs passed as next_page." + self.login() + response = self.client.get('/logout/next_page/named/') + self.assertEqual(response.status_code, 302) + self.assertURLEqual(response.url, '/password_reset/') self.confirm_logged_out() def test_security_check(self, password='password'): - logout_url = reverse('django.contrib.auth.views.logout') + logout_url = reverse('logout') # Those URLs should not pass the security check for bad_url in ('http://example.com', @@ -541,5 +682,7 @@ class ChangelistTests(AuthViewsTestCase): self.login() # A lookup that tries to filter on password isn't OK - with self.assertRaises(SuspiciousOperation): + with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls: response = self.client.get('/admin/auth/user/?password__startswith=sha1$') + self.assertEqual(response.status_code, 400) + self.assertEqual(len(logger_calls), 1) diff --git a/django/contrib/auth/tests/urls.py b/django/contrib/auth/tests/urls.py index 51b05be648..835ff41de7 100644 --- a/django/contrib/auth/tests/urls.py +++ b/django/contrib/auth/tests/urls.py @@ -62,8 +62,19 @@ def custom_request_auth_login(request): urlpatterns = urlpatterns + patterns('', (r'^logout/custom_query/$', 'django.contrib.auth.views.logout', dict(redirect_field_name='follow')), (r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')), + (r'^logout/next_page/named/$', 'django.contrib.auth.views.logout', dict(next_page='password_reset')), (r'^remote_user/$', remote_user_auth_view), (r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')), + (r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')), + (r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')), + (r'^reset/custom/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + 'django.contrib.auth.views.password_reset_confirm', + dict(post_reset_redirect='/custom/')), + (r'^reset/custom/named/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$', + 'django.contrib.auth.views.password_reset_confirm', + dict(post_reset_redirect='password_reset')), + (r'^password_change/custom/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='/custom/')), + (r'^password_change/custom/named/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='password_reset')), (r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)), (r'^login_required/$', login_required(password_reset)), (r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')), diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 8a554b0ad8..fe21683323 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -72,6 +72,9 @@ def logout(request, next_page=None, """ auth_logout(request) + if next_page is not None: + next_page = resolve_url(next_page) + if redirect_field_name in request.REQUEST: next_page = request.REQUEST[redirect_field_name] # Security check -- don't allow redirection to a different host. @@ -139,7 +142,9 @@ def password_reset(request, is_admin_site=False, current_app=None, extra_context=None): if post_reset_redirect is None: - post_reset_redirect = reverse('django.contrib.auth.views.password_reset_done') + post_reset_redirect = reverse('password_reset_done') + else: + post_reset_redirect = resolve_url(post_reset_redirect) if request.method == "POST": form = password_reset_form(request.POST) if form.is_valid(): @@ -192,7 +197,9 @@ def password_reset_confirm(request, uidb36=None, token=None, UserModel = get_user_model() assert uidb36 is not None and token is not None # checked by URLconf if post_reset_redirect is None: - post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete') + post_reset_redirect = reverse('password_reset_complete') + else: + post_reset_redirect = resolve_url(post_reset_redirect) try: uid_int = base36_to_int(uidb36) user = UserModel._default_manager.get(pk=uid_int) @@ -242,7 +249,9 @@ def password_change(request, password_change_form=PasswordChangeForm, current_app=None, extra_context=None): if post_change_redirect is None: - post_change_redirect = reverse('django.contrib.auth.views.password_change_done') + post_change_redirect = reverse('password_change_done') + else: + post_change_redirect = resolve_url(post_change_redirect) if request.method == "POST": form = password_change_form(user=request.user, data=request.POST) if form.is_valid(): diff --git a/django/contrib/comments/locale/en/LC_MESSAGES/django.po b/django/contrib/comments/locale/en/LC_MESSAGES/django.po index 6aca84c3cc..43ca058b6c 100644 --- a/django/contrib/comments/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/comments/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:18+0200\n" +"POT-Creation-Date: 2013-05-25 14:19+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English <en@li.org>\n" @@ -269,9 +269,11 @@ msgstr "" #: templates/comments/preview.html:11 msgid "Please correct the error below" -msgid_plural "Please correct the errors below" -msgstr[0] "" -msgstr[1] "" +msgstr "" + +#: templates/comments/preview.html:11 +msgid "Please correct the errors below" +msgstr "" #: templates/comments/preview.html:16 msgid "Post your comment" diff --git a/django/contrib/comments/templates/comments/preview.html b/django/contrib/comments/templates/comments/preview.html index 882d0fe714..0e7056795b 100644 --- a/django/contrib/comments/templates/comments/preview.html +++ b/django/contrib/comments/templates/comments/preview.html @@ -8,7 +8,7 @@ <form action="{% comment_form_target %}" method="post">{% csrf_token %} {% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %} {% if form.errors %} - <h1>{% blocktrans count counter=form.errors|length %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1> + <h1>{% if form.errors|length == 1 %}{% trans "Please correct the error below" %}{% else %}{% trans "Please correct the errors below" %}{% endif %}</h1> {% else %} <h1>{% trans "Preview your comment" %}</h1> <blockquote>{{ comment|linebreaks }}</blockquote> diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 399d24aa87..26db4ab171 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -35,9 +35,10 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): fields. """ - def __init__(self, ct_field="content_type", fk_field="object_id"): + def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True): self.ct_field = ct_field self.fk_field = fk_field + self.for_concrete_model = for_concrete_model def contribute_to_class(self, cls, name): self.name = name @@ -63,7 +64,8 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): def get_content_type(self, obj=None, id=None, using=None): if obj is not None: - return ContentType.objects.db_manager(obj._state.db).get_for_model(obj) + return ContentType.objects.db_manager(obj._state.db).get_for_model( + obj, for_concrete_model=self.for_concrete_model) elif id: return ContentType.objects.db_manager(using).get_for_id(id) else: @@ -160,6 +162,8 @@ class GenericRelation(ForeignObject): self.object_id_field_name = kwargs.pop("object_id_field", "object_id") self.content_type_field_name = kwargs.pop("content_type_field", "content_type") + self.for_concrete_model = kwargs.pop("for_concrete_model", True) + kwargs['blank'] = True kwargs['editable'] = False kwargs['serialize'] = False @@ -201,7 +205,7 @@ class GenericRelation(ForeignObject): # Save a reference to which model this class is on for future use self.model = cls # Add the descriptor for the relation - setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self)) + setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self, self.for_concrete_model)) def contribute_to_related_class(self, cls, related): pass @@ -216,7 +220,8 @@ class GenericRelation(ForeignObject): """ Returns the content type associated with this field's model. """ - return ContentType.objects.get_for_model(self.model) + return ContentType.objects.get_for_model(self.model, + for_concrete_model=self.for_concrete_model) def get_extra_restriction(self, where_class, alias, remote_alias): field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0] @@ -232,7 +237,8 @@ class GenericRelation(ForeignObject): """ return self.rel.to._base_manager.db_manager(using).filter(**{ "%s__pk" % self.content_type_field_name: - ContentType.objects.db_manager(using).get_for_model(self.model).pk, + ContentType.objects.db_manager(using).get_for_model( + self.model, for_concrete_model=self.for_concrete_model).pk, "%s__in" % self.object_id_field_name: [obj.pk for obj in objs] }) @@ -247,8 +253,9 @@ class ReverseGenericRelatedObjectsDescriptor(object): "article.publications", the publications attribute is a ReverseGenericRelatedObjectsDescriptor instance. """ - def __init__(self, field): + def __init__(self, field, for_concrete_model=True): self.field = field + self.for_concrete_model = for_concrete_model def __get__(self, instance, instance_type=None): if instance is None: @@ -261,7 +268,8 @@ class ReverseGenericRelatedObjectsDescriptor(object): RelatedManager = create_generic_related_manager(superclass) qn = connection.ops.quote_name - content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance) + content_type = ContentType.objects.db_manager(instance._state.db).get_for_model( + instance, for_concrete_model=self.for_concrete_model) join_cols = self.field.get_joining_columns(reverse_join=True)[0] manager = RelatedManager( @@ -376,7 +384,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet): """ def __init__(self, data=None, files=None, instance=None, save_as_new=None, - prefix=None, queryset=None): + prefix=None, queryset=None, **kwargs): opts = self.model._meta self.instance = instance self.rel_name = '-'.join(( @@ -389,12 +397,14 @@ class BaseGenericInlineFormSet(BaseModelFormSet): if queryset is None: queryset = self.model._default_manager qs = queryset.filter(**{ - self.ct_field.name: ContentType.objects.get_for_model(self.instance), + self.ct_field.name: ContentType.objects.get_for_model( + self.instance, for_concrete_model=self.for_concrete_model), self.ct_fk_field.name: self.instance.pk, }) super(BaseGenericInlineFormSet, self).__init__( queryset=qs, data=data, files=files, - prefix=prefix + prefix=prefix, + **kwargs ) @classmethod @@ -406,7 +416,8 @@ class BaseGenericInlineFormSet(BaseModelFormSet): def save_new(self, form, commit=True): kwargs = { - self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk, + self.ct_field.get_attname(): ContentType.objects.get_for_model( + self.instance, for_concrete_model=self.for_concrete_model).pk, self.ct_fk_field.get_attname(): self.instance.pk, } new_obj = self.model(**kwargs) @@ -418,7 +429,8 @@ def generic_inlineformset_factory(model, form=ModelForm, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, - formfield_callback=None, validate_max=False): + formfield_callback=None, validate_max=False, + for_concrete_model=True): """ Returns a ``GenericInlineFormSet`` for the given kwargs. @@ -444,6 +456,7 @@ def generic_inlineformset_factory(model, form=ModelForm, validate_max=validate_max) FormSet.ct_field = ct_field FormSet.ct_fk_field = fk_field + FormSet.for_concrete_model = for_concrete_model return FormSet class GenericInlineModelAdmin(InlineModelAdmin): diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index f0bd109b00..34d54441fe 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -118,11 +118,13 @@ class ContentTypeManager(models.Manager): def _add_to_cache(self, using, ct): """Insert a ContentType into the cache.""" - model = ct.model_class() - key = (model._meta.app_label, model._meta.model_name) + # Note it's possible for ContentType objects to be stale; model_class() will return None. + # Hence, there is no reliance on model._meta.app_label here, just using the model fields instead. + key = (ct.app_label, ct.model) self.__class__._cache.setdefault(using, {})[key] = ct self.__class__._cache.setdefault(using, {})[ct.id] = ct + @python_2_unicode_compatible class ContentType(models.Model): name = models.CharField(max_length=100) @@ -153,7 +155,6 @@ class ContentType(models.Model): def model_class(self): "Returns the Python model class for this type of content." - from django.db import models return models.get_model(self.app_label, self.model, only_installed=False) diff --git a/django/contrib/contenttypes/tests.py b/django/contrib/contenttypes/tests.py index 7937873a00..f300294cd6 100644 --- a/django/contrib/contenttypes/tests.py +++ b/django/contrib/contenttypes/tests.py @@ -274,3 +274,10 @@ class ContentTypesTests(TestCase): model = 'OldModel', ) self.assertEqual(six.text_type(ct), 'Old model') + self.assertIsNone(ct.model_class()) + + # Make sure stale ContentTypes can be fetched like any other object. + # Before Django 1.6 this caused a NoneType error in the caching mechanism. + # Instead, just return the ContentType object and let the app detect stale states. + ct_fetched = ContentType.objects.get_for_id(ct.pk) + self.assertIsNone(ct_fetched.model_class()) diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 497979e497..20e930f343 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -1,7 +1,6 @@ from django.conf import settings from django.contrib.flatpages.models import FlatPage from django.contrib.sites.models import get_current_site -from django.core.xheaders import populate_xheaders from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect from django.shortcuts import get_object_or_404 from django.template import loader, RequestContext @@ -70,5 +69,4 @@ def render_flatpage(request, f): 'flatpage': f, }) response = HttpResponse(t.render(c)) - populate_xheaders(request, response, FlatPage, f.id) return response diff --git a/django/contrib/formtools/exceptions.py b/django/contrib/formtools/exceptions.py new file mode 100644 index 0000000000..f07ac9f745 --- /dev/null +++ b/django/contrib/formtools/exceptions.py @@ -0,0 +1,6 @@ +from django.core.exceptions import SuspiciousOperation + + +class WizardViewCookieModified(SuspiciousOperation): + """Signature of cookie modified""" + pass diff --git a/django/contrib/formtools/wizard/storage/cookie.py b/django/contrib/formtools/wizard/storage/cookie.py index e80361042f..9bf6503f18 100644 --- a/django/contrib/formtools/wizard/storage/cookie.py +++ b/django/contrib/formtools/wizard/storage/cookie.py @@ -1,8 +1,8 @@ import json -from django.core.exceptions import SuspiciousOperation from django.core.signing import BadSignature +from django.contrib.formtools.exceptions import WizardViewCookieModified from django.contrib.formtools.wizard import storage @@ -21,7 +21,7 @@ class CookieStorage(storage.BaseStorage): except KeyError: data = None except BadSignature: - raise SuspiciousOperation('WizardView cookie manipulated') + raise WizardViewCookieModified('WizardView cookie manipulated') if data is None: return None return json.loads(data, cls=json.JSONDecoder) diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index 734f39c752..84dbda3239 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -11,6 +11,10 @@ from django.core.exceptions import ImproperlyConfigured from django.db.backends.postgresql_psycopg2.base import DatabaseOperations from django.db.utils import DatabaseError from django.utils import six +from django.utils.functional import cached_property + +from .models import GeometryColumns, SpatialRefSys + #### Classes used in constructing PostGIS spatial SQL #### class PostGISOperator(SpatialOperation): @@ -62,6 +66,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): compiler_module = 'django.contrib.gis.db.models.sql.compiler' name = 'postgis' postgis = True + geom_func_prefix = 'ST_' version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)') valid_aggregates = dict([(k, None) for k in ('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')]) @@ -72,45 +77,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): def __init__(self, connection): super(PostGISOperations, self).__init__(connection) - # Trying to get the PostGIS version because the function - # signatures will depend on the version used. The cost - # here is a database query to determine the version, which - # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple - # comprising user-supplied values for the major, minor, and - # subminor revision of PostGIS. - try: - if hasattr(settings, 'POSTGIS_VERSION'): - vtup = settings.POSTGIS_VERSION - if len(vtup) == 3: - # The user-supplied PostGIS version. - version = vtup - else: - # This was the old documented way, but it's stupid to - # include the string. - version = vtup[1:4] - else: - vtup = self.postgis_version_tuple() - version = vtup[1:] - - # Getting the prefix -- even though we don't officially support - # PostGIS 1.2 anymore, keeping it anyway in case a prefix change - # for something else is necessary. - if version >= (1, 2, 2): - prefix = 'ST_' - else: - prefix = '' - - self.geom_func_prefix = prefix - self.spatial_version = version - except DatabaseError: - raise ImproperlyConfigured( - 'Cannot determine PostGIS version for database "%s". ' - 'GeoDjango requires at least PostGIS version 1.3. ' - 'Was the database created from a spatial database ' - 'template?' % self.connection.settings_dict['NAME'] - ) - # TODO: Raise helpful exceptions as they become known. - + prefix = self.geom_func_prefix # PostGIS-specific operators. The commented descriptions of these # operators come from Section 7.6 of the PostGIS 1.4 documentation. self.geometry_operators = { @@ -188,13 +155,13 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.geometry_functions.update(self.distance_functions) # Only PostGIS versions 1.3.4+ have GeoJSON serialization support. - if version < (1, 3, 4): + if self.spatial_version < (1, 3, 4): GEOJSON = False else: GEOJSON = prefix + 'AsGeoJson' # ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4. - if version >= (1, 4, 0): + if self.spatial_version >= (1, 4, 0): GEOHASH = 'ST_GeoHash' BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle' self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly') @@ -202,7 +169,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): GEOHASH, BOUNDINGCIRCLE = False, False # Geography type support added in 1.5. - if version >= (1, 5, 0): + if self.spatial_version >= (1, 5, 0): self.geography = True # Only a subset of the operators and functions are available # for the geography type. @@ -217,7 +184,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): } # Native geometry type support added in PostGIS 2.0. - if version >= (2, 0, 0): + if self.spatial_version >= (2, 0, 0): self.geometry = True # Creating a dictionary lookup of all GIS terms for PostGIS. @@ -260,7 +227,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.union = prefix + 'Union' self.unionagg = prefix + 'Union' - if version >= (2, 0, 0): + if self.spatial_version >= (2, 0, 0): self.extent3d = prefix + '3DExtent' self.length3d = prefix + '3DLength' self.perimeter3d = prefix + '3DPerimeter' @@ -269,6 +236,30 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): self.length3d = prefix + 'Length3D' self.perimeter3d = prefix + 'Perimeter3D' + @cached_property + def spatial_version(self): + """Determine the version of the PostGIS library.""" + # Trying to get the PostGIS version because the function + # signatures will depend on the version used. The cost + # here is a database query to determine the version, which + # can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple + # comprising user-supplied values for the major, minor, and + # subminor revision of PostGIS. + if hasattr(settings, 'POSTGIS_VERSION'): + version = settings.POSTGIS_VERSION + else: + try: + vtup = self.postgis_version_tuple() + except DatabaseError: + raise ImproperlyConfigured( + 'Cannot determine PostGIS version for database "%s". ' + 'GeoDjango requires at least PostGIS version 1.3. ' + 'Was the database created from a spatial database ' + 'template?' % self.connection.settings_dict['NAME'] + ) + version = vtup[1:] + return version + def check_aggregate_support(self, aggregate): """ Checks if the given aggregate name is supported (that is, if it's @@ -572,9 +563,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations): # Routines for getting the OGC-compliant models. def geometry_columns(self): - from django.contrib.gis.db.backends.postgis.models import GeometryColumns return GeometryColumns def spatial_ref_sys(self): - from django.contrib.gis.db.backends.postgis.models import SpatialRefSys return SpatialRefSys diff --git a/django/contrib/gis/geos/geometry.py b/django/contrib/gis/geos/geometry.py index 01719c21d4..b088ec2dc4 100644 --- a/django/contrib/gis/geos/geometry.py +++ b/django/contrib/gis/geos/geometry.py @@ -384,7 +384,7 @@ class GEOSGeometry(GEOSBase, ListMixin): @property def wkt(self): "Returns the WKT (Well-Known Text) representation of this Geometry." - return wkt_w(self.hasz and 3 or 2).write(self).decode() + return wkt_w(3 if self.hasz else 2).write(self).decode() @property def hex(self): @@ -395,7 +395,7 @@ class GEOSGeometry(GEOSBase, ListMixin): """ # A possible faster, all-python, implementation: # str(self.wkb).encode('hex') - return wkb_w(self.hasz and 3 or 2).write_hex(self) + return wkb_w(3 if self.hasz else 2).write_hex(self) @property def hexewkb(self): @@ -407,7 +407,7 @@ class GEOSGeometry(GEOSBase, ListMixin): if self.hasz and not GEOS_PREPARE: # See: http://trac.osgeo.org/geos/ticket/216 raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.') - return ewkb_w(self.hasz and 3 or 2).write_hex(self) + return ewkb_w(3 if self.hasz else 2).write_hex(self) @property def json(self): @@ -427,7 +427,7 @@ class GEOSGeometry(GEOSBase, ListMixin): as a Python buffer. SRID and Z values are not included, use the `ewkb` property instead. """ - return wkb_w(self.hasz and 3 or 2).write(self) + return wkb_w(3 if self.hasz else 2).write(self) @property def ewkb(self): @@ -439,7 +439,7 @@ class GEOSGeometry(GEOSBase, ListMixin): if self.hasz and not GEOS_PREPARE: # See: http://trac.osgeo.org/geos/ticket/216 raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.') - return ewkb_w(self.hasz and 3 or 2).write(self) + return ewkb_w(3 if self.hasz else 2).write(self) @property def kml(self): diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index cb56b8bce2..a502aa280d 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -546,7 +546,7 @@ class LayerMapping(object): # Attempting to save. m.save(using=self.using) num_saved += 1 - if verbose: stream.write('%s: %s\n' % (is_update and 'Updated' or 'Saved', m)) + if verbose: stream.write('%s: %s\n' % ('Updated' if is_update else 'Saved', m)) except SystemExit: raise except Exception as msg: diff --git a/django/contrib/humanize/locale/en/LC_MESSAGES/django.po b/django/contrib/humanize/locale/en/LC_MESSAGES/django.po index fc75b677a0..2c3cd0c08d 100644 --- a/django/contrib/humanize/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/humanize/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:18+0200\n" +"POT-Creation-Date: 2013-05-18 23:10+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English <en@li.org>\n" @@ -237,54 +237,60 @@ msgctxt "naturaltime" msgid "%(delta)s ago" msgstr "" -#: templatetags/humanize.py:194 templatetags/humanize.py:216 +#: templatetags/humanize.py:194 templatetags/humanize.py:219 msgid "now" msgstr "" -#: templatetags/humanize.py:197 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:198 #, python-format msgid "a second ago" -msgid_plural "%(count)s seconds ago" +msgid_plural "%(count)s\\u00a0seconds ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:202 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:204 #, python-format msgid "a minute ago" -msgid_plural "%(count)s minutes ago" +msgid_plural "%(count)s\\u00a0minutes ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:207 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:210 #, python-format msgid "an hour ago" -msgid_plural "%(count)s hours ago" +msgid_plural "%(count)s\\u00a0hours ago" msgstr[0] "" msgstr[1] "" -#: templatetags/humanize.py:213 +#: templatetags/humanize.py:216 #, python-format msgctxt "naturaltime" msgid "%(delta)s from now" msgstr "" -#: templatetags/humanize.py:219 +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:223 #, python-format msgid "a second from now" -msgid_plural "%(count)s seconds from now" -msgstr[0] "" -msgstr[1] "" - -#: templatetags/humanize.py:224 -#, python-format -msgid "a minute from now" -msgid_plural "%(count)s minutes from now" +msgid_plural "%(count)s\\u00a0seconds from now" msgstr[0] "" msgstr[1] "" +#. Translators: \\u00a0 is non-breaking space #: templatetags/humanize.py:229 #, python-format -msgid "an hour from now" -msgid_plural "%(count)s hours from now" +msgid "a minute from now" +msgid_plural "%(count)s\\u00a0minutes from now" +msgstr[0] "" +msgstr[1] "" + +#. Translators: \\u00a0 is non-breaking space +#: templatetags/humanize.py:235 +#, python-format +msgid "an hour from now" +msgid_plural "%(count)s\\u00a0hours from now" msgstr[0] "" msgstr[1] "" diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 21f4c452fa..eaee734f75 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -194,17 +194,20 @@ def naturaltime(value): return _('now') elif delta.seconds < 60: return ungettext( - 'a second ago', '%(count)s seconds ago', delta.seconds + # Translators: \\u00a0 is non-breaking space + 'a second ago', '%(count)s\u00a0seconds ago', delta.seconds ) % {'count': delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return ungettext( - 'a minute ago', '%(count)s minutes ago', count + # Translators: \\u00a0 is non-breaking space + 'a minute ago', '%(count)s\u00a0minutes ago', count ) % {'count': count} else: count = delta.seconds // 60 // 60 return ungettext( - 'an hour ago', '%(count)s hours ago', count + # Translators: \\u00a0 is non-breaking space + 'an hour ago', '%(count)s\u00a0hours ago', count ) % {'count': count} else: delta = value - now @@ -216,15 +219,18 @@ def naturaltime(value): return _('now') elif delta.seconds < 60: return ungettext( - 'a second from now', '%(count)s seconds from now', delta.seconds + # Translators: \\u00a0 is non-breaking space + 'a second from now', '%(count)s\u00a0seconds from now', delta.seconds ) % {'count': delta.seconds} elif delta.seconds // 60 < 60: count = delta.seconds // 60 return ungettext( - 'a minute from now', '%(count)s minutes from now', count + # Translators: \\u00a0 is non-breaking space + 'a minute from now', '%(count)s\u00a0minutes from now', count ) % {'count': count} else: count = delta.seconds // 60 // 60 return ungettext( - 'an hour from now', '%(count)s hours from now', count + # Translators: \\u00a0 is non-breaking space + 'an hour from now', '%(count)s\u00a0hours from now', count ) % {'count': count} diff --git a/django/contrib/humanize/tests.py b/django/contrib/humanize/tests.py index 1e1c8424e6..54a60f8fd6 100644 --- a/django/contrib/humanize/tests.py +++ b/django/contrib/humanize/tests.py @@ -19,6 +19,8 @@ from django.utils.translation import ugettext as _ from django.utils import tzinfo from django.utils.unittest import skipIf +from i18n import TransRealMixin + # Mock out datetime in some tests so they don't fail occasionally when they # run too slow. Use a fixed datetime for datetime.now(). DST change in @@ -36,7 +38,7 @@ class MockDateTime(datetime.datetime): return now.replace(tzinfo=tz) + tz.utcoffset(now) -class HumanizeTests(TestCase): +class HumanizeTests(TransRealMixin, TestCase): def humanize_tester(self, test_list, result_list, method): for test_content, result in zip(test_list, result_list): @@ -195,22 +197,22 @@ class HumanizeTests(TestCase): result_list = [ 'now', 'a second ago', - '30 seconds ago', + '30\xa0seconds ago', 'a minute ago', - '2 minutes ago', + '2\xa0minutes ago', 'an hour ago', - '23 hours ago', - '1 day ago', - '1 year, 4 months ago', + '23\xa0hours ago', + '1\xa0day ago', + '1\xa0year, 4\xa0months ago', 'a second from now', - '30 seconds from now', + '30\xa0seconds from now', 'a minute from now', - '2 minutes from now', + '2\xa0minutes from now', 'an hour from now', - '23 hours from now', - '1 day from now', - '2 days, 6 hours from now', - '1 year, 4 months from now', + '23\xa0hours from now', + '1\xa0day from now', + '2\xa0days, 6\xa0hours from now', + '1\xa0year, 4\xa0months from now', 'now', 'now', ] @@ -218,8 +220,8 @@ class HumanizeTests(TestCase): # date in naive arithmetic is only 2 days and 5 hours after in # aware arithmetic. result_list_with_tz_support = result_list[:] - assert result_list_with_tz_support[-4] == '2 days, 6 hours from now' - result_list_with_tz_support[-4] == '2 days, 5 hours from now' + assert result_list_with_tz_support[-4] == '2\xa0days, 6\xa0hours from now' + result_list_with_tz_support[-4] == '2\xa0days, 5\xa0hours from now' orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime try: diff --git a/django/contrib/sessions/backends/base.py b/django/contrib/sessions/backends/base.py index f79a264500..759d7ac7ad 100644 --- a/django/contrib/sessions/backends/base.py +++ b/django/contrib/sessions/backends/base.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import base64 from datetime import datetime, timedelta +import logging + try: from django.utils.six.moves import cPickle as pickle except ImportError: @@ -14,7 +16,9 @@ from django.utils.crypto import constant_time_compare from django.utils.crypto import get_random_string from django.utils.crypto import salted_hmac from django.utils import timezone -from django.utils.encoding import force_bytes +from django.utils.encoding import force_bytes, force_text + +from django.contrib.sessions.exceptions import SuspiciousSession # session_key should not be case sensitive because some backends can store it # on case insensitive file systems. @@ -94,12 +98,16 @@ class SessionBase(object): hash, pickled = encoded_data.split(b':', 1) expected_hash = self._hash(pickled) if not constant_time_compare(hash.decode(), expected_hash): - raise SuspiciousOperation("Session data corrupted") + raise SuspiciousSession("Session data corrupted") else: return pickle.loads(pickled) - except Exception: + except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions. If any of # these happen, just return an empty dictionary (an empty session). + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) return {} def update(self, dict_): diff --git a/django/contrib/sessions/backends/cached_db.py b/django/contrib/sessions/backends/cached_db.py index 31c6fbfce3..be22c1f97a 100644 --- a/django/contrib/sessions/backends/cached_db.py +++ b/django/contrib/sessions/backends/cached_db.py @@ -2,10 +2,13 @@ Cached, database-backed sessions. """ +import logging + from django.contrib.sessions.backends.db import SessionStore as DBStore from django.core.cache import cache from django.core.exceptions import SuspiciousOperation from django.utils import timezone +from django.utils.encoding import force_text KEY_PREFIX = "django.contrib.sessions.cached_db" @@ -41,7 +44,11 @@ class SessionStore(DBStore): data = self.decode(s.session_data) cache.set(self.cache_key, data, self.get_expiry_age(expiry=s.expire_date)) - except (Session.DoesNotExist, SuspiciousOperation): + except (Session.DoesNotExist, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) self.create() data = {} return data diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py index 30da0b7a10..206fca2700 100644 --- a/django/contrib/sessions/backends/db.py +++ b/django/contrib/sessions/backends/db.py @@ -1,8 +1,10 @@ +import logging + from django.contrib.sessions.backends.base import SessionBase, CreateError from django.core.exceptions import SuspiciousOperation from django.db import IntegrityError, transaction, router from django.utils import timezone - +from django.utils.encoding import force_text class SessionStore(SessionBase): """ @@ -18,7 +20,11 @@ class SessionStore(SessionBase): expire_date__gt=timezone.now() ) return self.decode(s.session_data) - except (Session.DoesNotExist, SuspiciousOperation): + except (Session.DoesNotExist, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) self.create() return {} diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py index 9588680fea..f47aa2d867 100644 --- a/django/contrib/sessions/backends/file.py +++ b/django/contrib/sessions/backends/file.py @@ -1,5 +1,6 @@ import datetime import errno +import logging import os import shutil import tempfile @@ -8,6 +9,9 @@ from django.conf import settings from django.contrib.sessions.backends.base import SessionBase, CreateError, VALID_KEY_CHARS from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.utils import timezone +from django.utils.encoding import force_text + +from django.contrib.sessions.exceptions import InvalidSessionKey class SessionStore(SessionBase): """ @@ -48,7 +52,7 @@ class SessionStore(SessionBase): # should always be md5s, so they should never contain directory # components. if not set(session_key).issubset(set(VALID_KEY_CHARS)): - raise SuspiciousOperation( + raise InvalidSessionKey( "Invalid characters in session key") return os.path.join(self.storage_path, self.file_prefix + session_key) @@ -75,7 +79,11 @@ class SessionStore(SessionBase): if file_data: try: session_data = self.decode(file_data) - except (EOFError, SuspiciousOperation): + except (EOFError, SuspiciousOperation) as e: + if isinstance(e, SuspiciousOperation): + logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + logger.warning(force_text(e)) self.create() # Remove expired sessions. @@ -86,7 +94,7 @@ class SessionStore(SessionBase): session_data = {} self.delete() self.create() - except IOError: + except (IOError, SuspiciousOperation): self.create() return session_data diff --git a/django/contrib/sessions/exceptions.py b/django/contrib/sessions/exceptions.py new file mode 100644 index 0000000000..4f4dc6b048 --- /dev/null +++ b/django/contrib/sessions/exceptions.py @@ -0,0 +1,11 @@ +from django.core.exceptions import SuspiciousOperation + + +class InvalidSessionKey(SuspiciousOperation): + """Invalid characters in session key""" + pass + + +class SuspiciousSession(SuspiciousOperation): + """The session may be tampered with""" + pass diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index 8bcc505ee6..cd8191a6a4 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -1,3 +1,4 @@ +import base64 from datetime import timedelta import os import shutil @@ -15,14 +16,16 @@ from django.contrib.sessions.models import Session from django.contrib.sessions.middleware import SessionMiddleware from django.core.cache import get_cache from django.core import management -from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation +from django.core.exceptions import ImproperlyConfigured from django.http import HttpResponse from django.test import TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, patch_logger from django.utils import six from django.utils import timezone from django.utils import unittest +from django.contrib.sessions.exceptions import InvalidSessionKey + class SessionTestsMixin(object): # This does not inherit from TestCase to avoid any tests being run with this @@ -272,6 +275,15 @@ class SessionTestsMixin(object): encoded = self.session.encode(data) self.assertEqual(self.session.decode(encoded), data) + def test_decode_failure_logged_to_security(self): + bad_encode = base64.b64encode(b'flaskdj:alkdjf') + with patch_logger('django.security.SuspiciousSession', 'warning') as calls: + self.assertEqual({}, self.session.decode(bad_encode)) + # check that the failed decode is logged + self.assertEqual(len(calls), 1) + self.assertTrue('corrupted' in calls[0]) + + def test_actual_expiry(self): # Regression test for #19200 old_session_key = None @@ -403,14 +415,21 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase): self.assertRaises(ImproperlyConfigured, self.backend) def test_invalid_key_backslash(self): - # Ensure we don't allow directory-traversal - self.assertRaises(SuspiciousOperation, - self.backend("a\\b\\c").load) + # This key should be refused and a new session should be created + self.assertTrue(self.backend("a\\b\\c").load()) + + def test_invalid_key_backslash(self): + # Ensure we don't allow directory-traversal. + # This is tested directly on _key_to_file, as load() will swallow + # a SuspiciousOperation in the same way as an IOError - by creating + # a new session, making it unclear whether the slashes were detected. + self.assertRaises(InvalidSessionKey, + self.backend()._key_to_file, "a\\b\\c") def test_invalid_key_forwardslash(self): # Ensure we don't allow directory-traversal - self.assertRaises(SuspiciousOperation, - self.backend("a/b/c").load) + self.assertRaises(InvalidSessionKey, + self.backend()._key_to_file, "a/b/c") @override_settings(SESSION_ENGINE="django.contrib.sessions.backends.file") def test_clearsessions_command(self): diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index 7d03ef19f5..1acae8296c 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -94,7 +94,7 @@ class Sitemap(object): 'location': loc, 'lastmod': self.__get('lastmod', item, None), 'changefreq': self.__get('changefreq', item, None), - 'priority': str(priority is not None and priority or ''), + 'priority': str(priority if priority is not None else ''), } urls.append(url_info) return urls diff --git a/django/contrib/staticfiles/finders.py b/django/contrib/staticfiles/finders.py index 8631570e79..7d266d95a0 100644 --- a/django/contrib/staticfiles/finders.py +++ b/django/contrib/staticfiles/finders.py @@ -245,7 +245,7 @@ def find(path, all=False): if matches: return matches # No match. - return all and [] or None + return [] if all else None def get_finders(): diff --git a/django/contrib/staticfiles/management/commands/collectstatic.py b/django/contrib/staticfiles/management/commands/collectstatic.py index c620993f33..7c3de80e93 100644 --- a/django/contrib/staticfiles/management/commands/collectstatic.py +++ b/django/contrib/staticfiles/management/commands/collectstatic.py @@ -175,11 +175,9 @@ Type 'yes' to continue, or 'no' to cancel: """ summary = template % { 'modified_count': modified_count, 'identifier': 'static file' + ('' if modified_count == 1 else 's'), - 'action': self.symlink and 'symlinked' or 'copied', - 'destination': (destination_path and " to '%s'" - % destination_path or ''), - 'unmodified': (collected['unmodified'] and ', %s unmodified' - % unmodified_count or ''), + 'action': 'symlinked' if self.symlink else 'copied', + 'destination': (" to '%s'" % destination_path if destination_path else ''), + 'unmodified': (', %s unmodified' % unmodified_count if collected['unmodified'] else ''), 'post_processed': (collected['post_processed'] and ', %s post-processed' % post_processed_count or ''), diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index 53b0270a57..deb98e7714 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -15,6 +15,10 @@ class CacheKeyWarning(DjangoRuntimeWarning): pass +# Stub class to ensure not passing in a `timeout` argument results in +# the default timeout +DEFAULT_TIMEOUT = object() + # Memcached does not accept keys longer than this. MEMCACHE_MAX_KEY_LENGTH = 250 @@ -84,7 +88,7 @@ class BaseCache(object): new_key = self.key_func(key, self.key_prefix, version) return new_key - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): """ Set a value in the cache if the key does not already exist. If timeout is given, that timeout will be used for the key; otherwise @@ -101,7 +105,7 @@ class BaseCache(object): """ raise NotImplementedError - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): """ Set a value in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. @@ -163,7 +167,7 @@ class BaseCache(object): # if a subclass overrides it. return self.has_key(key) - def set_many(self, data, timeout=None, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): """ Set a bunch of values in the cache at once from a dict of key/value pairs. For certain backends (memcached), this is much more efficient diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 7749284122..0e59c6d65e 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -9,7 +9,7 @@ except ImportError: import pickle from django.conf import settings -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.db import connections, transaction, router, DatabaseError from django.utils import timezone, six from django.utils.encoding import force_bytes @@ -65,6 +65,7 @@ class DatabaseCache(BaseDatabaseCache): if row is None: return default now = timezone.now() + if row[2] < now: db = router.db_for_write(self.cache_model_class) cursor = connections[db].cursor() @@ -74,18 +75,18 @@ class DatabaseCache(BaseDatabaseCache): value = connections[db].ops.process_clob(row[1]) return pickle.loads(base64.b64decode(force_bytes(value))) - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) self._base_set('set', key, value, timeout) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) return self._base_set('add', key, value, timeout) - def _base_set(self, mode, key, value, timeout=None): - if timeout is None: + def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT): + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout db = router.db_for_write(self.cache_model_class) table = connections[db].ops.quote_name(self._table) @@ -95,7 +96,9 @@ class DatabaseCache(BaseDatabaseCache): num = cursor.fetchone()[0] now = timezone.now() now = now.replace(microsecond=0) - if settings.USE_TZ: + if timeout is None: + exp = datetime.max + elif settings.USE_TZ: exp = datetime.utcfromtimestamp(time.time() + timeout) else: exp = datetime.fromtimestamp(time.time() + timeout) diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py index 9fe9b3b5f5..7ca6114ee4 100644 --- a/django/core/cache/backends/dummy.py +++ b/django/core/cache/backends/dummy.py @@ -1,12 +1,12 @@ "Dummy cache backend" -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT class DummyCache(BaseCache): def __init__(self, host, *args, **kwargs): BaseCache.__init__(self, *args, **kwargs) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) return True @@ -16,7 +16,7 @@ class DummyCache(BaseCache): self.validate_key(key) return default - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) @@ -32,7 +32,7 @@ class DummyCache(BaseCache): self.validate_key(key) return False - def set_many(self, data, timeout=0, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): pass def delete_many(self, keys, version=None): diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index 96194d458f..d19eed4a95 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -9,9 +9,10 @@ try: except ImportError: import pickle -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils.encoding import force_bytes + class FileBasedCache(BaseCache): def __init__(self, dir, params): BaseCache.__init__(self, params) @@ -19,7 +20,7 @@ class FileBasedCache(BaseCache): if not os.path.exists(self._dir): self._createdir() - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): if self.has_key(key, version=version): return False @@ -35,7 +36,7 @@ class FileBasedCache(BaseCache): with open(fname, 'rb') as f: exp = pickle.load(f) now = time.time() - if exp < now: + if exp is not None and exp < now: self._delete(fname) else: return pickle.load(f) @@ -43,14 +44,14 @@ class FileBasedCache(BaseCache): pass return default - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) fname = self._key_to_file(key) dirname = os.path.dirname(fname) - if timeout is None: + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout self._cull() @@ -60,8 +61,8 @@ class FileBasedCache(BaseCache): os.makedirs(dirname) with open(fname, 'wb') as f: - now = time.time() - pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL) + expiry = None if timeout is None else time.time() + timeout + pickle.dump(expiry, f, pickle.HIGHEST_PROTOCOL) pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) except (IOError, OSError): pass diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py index 76667e9609..1fa17052fd 100644 --- a/django/core/cache/backends/locmem.py +++ b/django/core/cache/backends/locmem.py @@ -6,7 +6,7 @@ try: except ImportError: import pickle -from django.core.cache.backends.base import BaseCache +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils.synch import RWLock # Global in-memory store of cache data. Keyed by name, to provide @@ -23,7 +23,7 @@ class LocMemCache(BaseCache): self._expire_info = _expire_info.setdefault(name, {}) self._lock = _locks.setdefault(name, RWLock()) - def add(self, key, value, timeout=None, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.writer(): @@ -41,10 +41,8 @@ class LocMemCache(BaseCache): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.reader(): - exp = self._expire_info.get(key) - if exp is None: - return default - elif exp > time.time(): + exp = self._expire_info.get(key, 0) + if exp is None or exp > time.time(): try: pickled = self._cache[key] return pickle.loads(pickled) @@ -58,15 +56,16 @@ class LocMemCache(BaseCache): pass return default - def _set(self, key, value, timeout=None): + def _set(self, key, value, timeout=DEFAULT_TIMEOUT): if len(self._cache) >= self._max_entries: self._cull() - if timeout is None: + if timeout == DEFAULT_TIMEOUT: timeout = self.default_timeout + expiry = None if timeout is None else time.time() + timeout self._cache[key] = value - self._expire_info[key] = time.time() + timeout + self._expire_info[key] = expiry - def set(self, key, value, timeout=None, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self.validate_key(key) with self._lock.writer(): diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index c942acd52f..64d1c41dc5 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -4,7 +4,7 @@ import time import pickle from threading import local -from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError +from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT from django.utils import six from django.utils.encoding import force_str @@ -36,12 +36,22 @@ class BaseMemcachedCache(BaseCache): return self._client - def _get_memcache_timeout(self, timeout): + def _get_memcache_timeout(self, timeout=DEFAULT_TIMEOUT): """ Memcached deals with long (> 30 days) timeouts in a special way. Call this function to obtain a safe value for your timeout. """ - timeout = timeout or self.default_timeout + if timeout == DEFAULT_TIMEOUT: + return self.default_timeout + + if timeout is None: + # Using 0 in memcache sets a non-expiring timeout. + return 0 + elif int(timeout) == 0: + # Other cache backends treat 0 as set-and-expire. To achieve this + # in memcache backends, a negative timeout must be passed. + timeout = -1 + if timeout > 2592000: # 60*60*24*30, 30 days # See http://code.google.com/p/memcached/wiki/FAQ # "You can set expire times up to 30 days in the future. After that @@ -56,7 +66,7 @@ class BaseMemcachedCache(BaseCache): # Python 2 memcache requires the key to be a byte string. return force_str(super(BaseMemcachedCache, self).make_key(key, version)) - def add(self, key, value, timeout=0, version=None): + def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) return self._cache.add(key, value, self._get_memcache_timeout(timeout)) @@ -67,7 +77,7 @@ class BaseMemcachedCache(BaseCache): return default return val - def set(self, key, value, timeout=0, version=None): + def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None): key = self.make_key(key, version=version) self._cache.set(key, value, self._get_memcache_timeout(timeout)) @@ -125,7 +135,7 @@ class BaseMemcachedCache(BaseCache): raise ValueError("Key '%s' not found" % key) return val - def set_many(self, data, timeout=0, version=None): + def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None): safe_data = {} for key, value in data.items(): key = self.make_key(key, version=version) diff --git a/django/core/exceptions.py b/django/core/exceptions.py index 233af40f88..2c79736e33 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -1,6 +1,7 @@ """ Global Django exception and warning classes. """ +import logging from functools import reduce @@ -9,37 +10,56 @@ class DjangoRuntimeWarning(RuntimeWarning): class ObjectDoesNotExist(Exception): - "The requested object does not exist" + """The requested object does not exist""" silent_variable_failure = True class MultipleObjectsReturned(Exception): - "The query returned multiple objects when only one was expected." + """The query returned multiple objects when only one was expected.""" pass class SuspiciousOperation(Exception): - "The user did something suspicious" + """The user did something suspicious""" + + +class SuspiciousMultipartForm(SuspiciousOperation): + """Suspect MIME request in multipart form data""" + pass + + +class SuspiciousFileOperation(SuspiciousOperation): + """A Suspicious filesystem operation was attempted""" + pass + + +class DisallowedHost(SuspiciousOperation): + """HTTP_HOST header contains invalid value""" + pass + + +class DisallowedRedirect(SuspiciousOperation): + """Redirect to scheme not in allowed list""" pass class PermissionDenied(Exception): - "The user did not have permission to do that" + """The user did not have permission to do that""" pass class ViewDoesNotExist(Exception): - "The requested view does not exist" + """The requested view does not exist""" pass class MiddlewareNotUsed(Exception): - "This middleware is not used in this server configuration" + """This middleware is not used in this server configuration""" pass class ImproperlyConfigured(Exception): - "Django is somehow improperly configured" + """Django is somehow improperly configured""" pass diff --git a/django/core/files/locks.py b/django/core/files/locks.py index d1384329dc..6f0e4b9508 100644 --- a/django/core/files/locks.py +++ b/django/core/files/locks.py @@ -41,7 +41,7 @@ except (ImportError, AttributeError): def fd(f): """Get a filedescriptor from something which could be a file or an fd.""" - return hasattr(f, 'fileno') and f.fileno() or f + return f.fileno() if hasattr(f, 'fileno') else f if system_type == 'nt': def lock(file, flags): diff --git a/django/core/files/move.py b/django/core/files/move.py index 3af02634fe..4519dedf97 100644 --- a/django/core/files/move.py +++ b/django/core/files/move.py @@ -62,7 +62,7 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove with open(old_file_name, 'rb') as old_file: # now open the new file, not forgetting allow_overwrite fd = os.open(new_file_name, os.O_WRONLY | os.O_CREAT | getattr(os, 'O_BINARY', 0) | - (not allow_overwrite and os.O_EXCL or 0)) + (os.O_EXCL if not allow_overwrite else 0)) try: locks.lock(fd, locks.LOCK_EX) current_chunk = None @@ -77,8 +77,8 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove try: os.remove(old_file_name) except OSError as e: - # Certain operating systems (Cygwin and Windows) - # fail when deleting opened files, ignore it. (For the + # Certain operating systems (Cygwin and Windows) + # fail when deleting opened files, ignore it. (For the # systems where this happens, temporary files will be auto-deleted # on close anyway.) if getattr(e, 'winerror', 0) != 32 and getattr(e, 'errno', 0) != 13: diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 18d15e1ab6..977b6a68a8 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -8,7 +8,7 @@ import itertools from datetime import datetime from django.conf import settings -from django.core.exceptions import SuspiciousOperation +from django.core.exceptions import SuspiciousFileOperation from django.core.files import locks, File from django.core.files.move import file_move_safe from django.utils.encoding import force_text, filepath_to_uri @@ -260,7 +260,7 @@ class FileSystemStorage(Storage): try: path = safe_join(self.location, name) except ValueError: - raise SuspiciousOperation("Attempted access to '%s' denied." % name) + raise SuspiciousFileOperation("Attempted access to '%s' denied." % name) return os.path.normpath(path) def size(self, name): diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index acc74db6f5..59118656c6 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -8,7 +8,7 @@ from django import http from django.conf import settings from django.core import urlresolvers from django.core import signals -from django.core.exceptions import MiddlewareNotUsed, PermissionDenied +from django.core.exceptions import MiddlewareNotUsed, PermissionDenied, SuspiciousOperation from django.db import connections, transaction from django.utils.encoding import force_text from django.utils.module_loading import import_by_path @@ -66,10 +66,11 @@ class BaseHandler(object): self._request_middleware = request_middleware def make_view_atomic(self, view): - if getattr(view, 'transactions_per_request', True): - for db in connections.all(): - if db.settings_dict['ATOMIC_REQUESTS']: - view = transaction.atomic(using=db.alias)(view) + non_atomic_requests = getattr(view, '_non_atomic_requests', set()) + for db in connections.all(): + if (db.settings_dict['ATOMIC_REQUESTS'] + and db.alias not in non_atomic_requests): + view = transaction.atomic(using=db.alias)(view) return view def get_response(self, request): @@ -169,11 +170,27 @@ class BaseHandler(object): response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) + except SuspiciousOperation as e: + # The request logger receives events for any problematic request + # The security logger receives events for all SuspiciousOperations + security_logger = logging.getLogger('django.security.%s' % + e.__class__.__name__) + security_logger.error(force_text(e)) + + try: + callback, param_dict = resolver.resolve400() + response = callback(request, **param_dict) + except: + signals.got_request_exception.send( + sender=self.__class__, request=request) + response = self.handle_uncaught_exception(request, + resolver, sys.exc_info()) + except SystemExit: # Allow sys.exit() to actually exit. See tickets #1023 and #4701 raise - except: # Handle everything else, including SuspiciousOperation, etc. + except: # Handle everything else. # Get the exception info now, in case another exception is thrown later. signals.got_request_exception.send(sender=self.__class__, request=request) response = self.handle_uncaught_exception(request, resolver, sys.exc_info()) diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index c348c6c8da..af78d1d269 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -13,67 +13,12 @@ from django.core.urlresolvers import set_script_prefix from django.utils import datastructures from django.utils.encoding import force_str, force_text, iri_to_uri +# For backwards compatibility -- lots of code uses this in the wild! +from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT + logger = logging.getLogger('django.request') -# See http://www.iana.org/assignments/http-status-codes -STATUS_CODE_TEXT = { - 100: 'CONTINUE', - 101: 'SWITCHING PROTOCOLS', - 102: 'PROCESSING', - 200: 'OK', - 201: 'CREATED', - 202: 'ACCEPTED', - 203: 'NON-AUTHORITATIVE INFORMATION', - 204: 'NO CONTENT', - 205: 'RESET CONTENT', - 206: 'PARTIAL CONTENT', - 207: 'MULTI-STATUS', - 208: 'ALREADY REPORTED', - 226: 'IM USED', - 300: 'MULTIPLE CHOICES', - 301: 'MOVED PERMANENTLY', - 302: 'FOUND', - 303: 'SEE OTHER', - 304: 'NOT MODIFIED', - 305: 'USE PROXY', - 306: 'RESERVED', - 307: 'TEMPORARY REDIRECT', - 400: 'BAD REQUEST', - 401: 'UNAUTHORIZED', - 402: 'PAYMENT REQUIRED', - 403: 'FORBIDDEN', - 404: 'NOT FOUND', - 405: 'METHOD NOT ALLOWED', - 406: 'NOT ACCEPTABLE', - 407: 'PROXY AUTHENTICATION REQUIRED', - 408: 'REQUEST TIMEOUT', - 409: 'CONFLICT', - 410: 'GONE', - 411: 'LENGTH REQUIRED', - 412: 'PRECONDITION FAILED', - 413: 'REQUEST ENTITY TOO LARGE', - 414: 'REQUEST-URI TOO LONG', - 415: 'UNSUPPORTED MEDIA TYPE', - 416: 'REQUESTED RANGE NOT SATISFIABLE', - 417: 'EXPECTATION FAILED', - 418: "I'M A TEAPOT", - 422: 'UNPROCESSABLE ENTITY', - 423: 'LOCKED', - 424: 'FAILED DEPENDENCY', - 426: 'UPGRADE REQUIRED', - 500: 'INTERNAL SERVER ERROR', - 501: 'NOT IMPLEMENTED', - 502: 'BAD GATEWAY', - 503: 'SERVICE UNAVAILABLE', - 504: 'GATEWAY TIMEOUT', - 505: 'HTTP VERSION NOT SUPPORTED', - 506: 'VARIANT ALSO NEGOTIATES', - 507: 'INSUFFICIENT STORAGE', - 508: 'LOOP DETECTED', - 510: 'NOT EXTENDED', -} - class LimitedStream(object): ''' LimitedStream wraps another stream in order to not allow reading from it @@ -254,11 +199,7 @@ class WSGIHandler(base.BaseHandler): response._handler_class = self.__class__ - try: - status_text = STATUS_CODE_TEXT[response.status_code] - except KeyError: - status_text = 'UNKNOWN STATUS CODE' - status = '%s %s' % (response.status_code, status_text) + status = '%s %s' % (response.status_code, response.reason_phrase) response_headers = [(str(k), str(v)) for k, v in response.items()] for c in response.cookies.values(): response_headers.append((str('Set-Cookie'), str(c.output(header='')))) diff --git a/django/core/management/base.py b/django/core/management/base.py index ba6ad8f4c0..af040288d0 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -7,7 +7,6 @@ import os import sys from optparse import make_option, OptionParser -import traceback import django from django.core.exceptions import ImproperlyConfigured @@ -171,7 +170,7 @@ class BaseCommand(object): make_option('--pythonpath', help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'), make_option('--traceback', action='store_true', - help='Print traceback on exception'), + help='Raise on exception'), ) help = '' args = '' @@ -231,7 +230,8 @@ class BaseCommand(object): Set up any environment changes requested (e.g., Python path and Django settings), then run this command. If the command raises a ``CommandError``, intercept it and print it sensibly - to stderr. + to stderr. If the ``--traceback`` option is present or the raised + ``Exception`` is not ``CommandError``, raise it. """ parser = self.create_parser(argv[0], argv[1]) options, args = parser.parse_args(argv[2:]) @@ -239,12 +239,12 @@ class BaseCommand(object): try: self.execute(*args, **options.__dict__) except Exception as e: + if options.traceback or not isinstance(e, CommandError): + raise + # self.stderr is not guaranteed to be set here stderr = getattr(self, 'stderr', OutputWrapper(sys.stderr, self.style.ERROR)) - if options.traceback or not isinstance(e, CommandError): - stderr.write(traceback.format_exc()) - else: - stderr.write('%s: %s' % (e.__class__.__name__, e)) + stderr.write('%s: %s' % (e.__class__.__name__, e)) sys.exit(1) def execute(self, *args, **options): diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index 4d1bc0403e..d7ce3e93fd 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -38,7 +38,7 @@ class Command(LabelCommand): qn = connection.ops.quote_name for f in fields: field_output = [qn(f.name), f.db_type(connection=connection)] - field_output.append("%sNULL" % (not f.null and "NOT " or "")) + field_output.append("%sNULL" % ("NOT " if not f.null else "")) if f.primary_key: field_output.append("PRIMARY KEY") elif f.unique: @@ -51,7 +51,7 @@ class Command(LabelCommand): table_output.append(" ".join(field_output)) full_statement = ["CREATE TABLE %s (" % qn(tablename)] for i, line in enumerate(table_output): - full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + full_statement.append(' %s%s' % (line, ',' if i < len(table_output)-1 else '')) full_statement.append(');') with transaction.commit_on_success_unless_managed(): curs = connection.cursor() diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index d3650b1eb8..c5eb1b9a9e 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -21,6 +21,9 @@ class Command(BaseCommand): help='Use natural keys if they are available.'), make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False, help="Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager."), + make_option('--pks', dest='primary_keys', help="Only dump objects with " + "given primary keys. Accepts a comma seperated list of keys. " + "This option will only work when you specify one model."), ) help = ("Output the contents of the database as a fixture of the given " "format (using each model's default manager unless --all is " @@ -37,6 +40,12 @@ class Command(BaseCommand): show_traceback = options.get('traceback') use_natural_keys = options.get('use_natural_keys') use_base_manager = options.get('use_base_manager') + pks = options.get('primary_keys') + + if pks: + primary_keys = pks.split(',') + else: + primary_keys = [] excluded_apps = set() excluded_models = set() @@ -55,8 +64,12 @@ class Command(BaseCommand): raise CommandError('Unknown app in excludes: %s' % exclude) if len(app_labels) == 0: + if primary_keys: + raise CommandError("You can only use --pks option with one model") app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps) else: + if len(app_labels) > 1 and primary_keys: + raise CommandError("You can only use --pks option with one model") app_list = SortedDict() for label in app_labels: try: @@ -77,6 +90,8 @@ class Command(BaseCommand): else: app_list[app] = [model] except ValueError: + if primary_keys: + raise CommandError("You can only use --pks option with one model") # This is just an app - no model qualifier app_label = label try: @@ -107,8 +122,11 @@ class Command(BaseCommand): objects = model._base_manager else: objects = model._default_manager - for obj in objects.using(using).\ - order_by(model._meta.pk.name).iterator(): + + queryset = objects.using(using).order_by(model._meta.pk.name) + if primary_keys: + queryset = queryset.filter(pk__in=primary_keys) + for obj in queryset.iterator(): yield obj try: diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 10066417a1..c56fc1e1b0 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -20,7 +20,7 @@ class Command(NoArgsCommand): default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. ' 'Defaults to the "default" database.'), make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True, - help='Tells Django not to load any initial data after database synchronization.'), + help='Tells Django not to load any initial data after database synchronization.'), ) help = ('Returns the database to the state it was in immediately after ' 'syncdb was executed. This means that all data will be removed ' diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index c95d11cf60..6856e85e45 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -1,9 +1,11 @@ from __future__ import unicode_literals -import os +import glob import gzip +import os import zipfile from optparse import make_option +import warnings from django.conf import settings from django.core import serializers @@ -11,8 +13,9 @@ from django.core.management.base import BaseCommand, CommandError from django.core.management.color import no_style from django.db import (connections, router, transaction, DEFAULT_DB_ALIAS, IntegrityError, DatabaseError) -from django.db.models import get_apps +from django.db.models import get_app_paths from django.utils.encoding import force_text +from django.utils.functional import cached_property, memoize from django.utils._os import upath from itertools import product @@ -43,9 +46,8 @@ class Command(BaseCommand): if not len(fixture_labels): raise CommandError( - "No database fixture specified. Please provide the path of at " - "least one fixture in the command line." - ) + "No database fixture specified. Please provide the path " + "of at least one fixture in the command line.") self.verbosity = int(options.get('verbosity')) @@ -68,37 +70,18 @@ class Command(BaseCommand): self.fixture_object_count = 0 self.models = set() - class SingleZipReader(zipfile.ZipFile): - def __init__(self, *args, **kwargs): - zipfile.ZipFile.__init__(self, *args, **kwargs) - if settings.DEBUG: - assert len(self.namelist()) == 1, "Zip-compressed fixtures must contain only one file." - def read(self): - return zipfile.ZipFile.read(self, self.namelist()[0]) - - self.compression_types = { + self.serialization_formats = serializers.get_public_serializer_formats() + self.compression_formats = { None: open, 'gz': gzip.GzipFile, 'zip': SingleZipReader } if has_bz2: - self.compression_types['bz2'] = bz2.BZ2File - - app_module_paths = [] - for app in get_apps(): - if hasattr(app, '__path__'): - # It's a 'models/' subpackage - for path in app.__path__: - app_module_paths.append(upath(path)) - else: - # It's a models.py module - app_module_paths.append(upath(app.__file__)) - - app_fixtures = [os.path.join(os.path.dirname(path), 'fixtures') for path in app_module_paths] + self.compression_formats['bz2'] = bz2.BZ2File with connection.constraint_checks_disabled(): for fixture_label in fixture_labels: - self.load_label(fixture_label, app_fixtures) + self.load_label(fixture_label) # Since we disabled constraint checks, we must manually check for # any invalid keys that might have been added @@ -123,122 +106,174 @@ class Command(BaseCommand): if self.verbosity >= 1: if self.fixture_object_count == self.loaded_object_count: - self.stdout.write("Installed %d object(s) from %d fixture(s)" % ( - self.loaded_object_count, self.fixture_count)) + self.stdout.write("Installed %d object(s) from %d fixture(s)" % + (self.loaded_object_count, self.fixture_count)) else: - self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" % ( - self.loaded_object_count, self.fixture_object_count, self.fixture_count)) + self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" % + (self.loaded_object_count, self.fixture_object_count, self.fixture_count)) - def load_label(self, fixture_label, app_fixtures): + def load_label(self, fixture_label): + """ + Loads fixtures files for a given label. + """ + for fixture_file, fixture_dir, fixture_name in self.find_fixtures(fixture_label): + _, ser_fmt, cmp_fmt = self.parse_name(os.path.basename(fixture_file)) + open_method = self.compression_formats[cmp_fmt] + fixture = open_method(fixture_file, 'r') + try: + self.fixture_count += 1 + objects_in_fixture = 0 + loaded_objects_in_fixture = 0 + if self.verbosity >= 2: + self.stdout.write("Installing %s fixture '%s' from %s." % + (ser_fmt, fixture_name, humanize(fixture_dir))) - parts = fixture_label.split('.') + objects = serializers.deserialize(ser_fmt, fixture, + using=self.using, ignorenonexistent=self.ignore) - if len(parts) > 1 and parts[-1] in self.compression_types: - compression_formats = [parts[-1]] - parts = parts[:-1] - else: - compression_formats = self.compression_types.keys() + for obj in objects: + objects_in_fixture += 1 + if router.allow_syncdb(self.using, obj.object.__class__): + loaded_objects_in_fixture += 1 + self.models.add(obj.object.__class__) + try: + obj.save(using=self.using) + except (DatabaseError, IntegrityError) as e: + e.args = ("Could not load %(app_label)s.%(object_name)s(pk=%(pk)s): %(error_msg)s" % { + 'app_label': obj.object._meta.app_label, + 'object_name': obj.object._meta.object_name, + 'pk': obj.object.pk, + 'error_msg': force_text(e) + },) + raise - if len(parts) == 1: - fixture_name = parts[0] - formats = serializers.get_public_serializer_formats() - else: - fixture_name, format = '.'.join(parts[:-1]), parts[-1] - if format in serializers.get_public_serializer_formats(): - formats = [format] - else: - formats = [] + self.loaded_object_count += loaded_objects_in_fixture + self.fixture_object_count += objects_in_fixture + except Exception as e: + if not isinstance(e, CommandError): + e.args = ("Problem installing fixture '%s': %s" % (fixture_file, e),) + raise + finally: + fixture.close() - if formats: - if self.verbosity >= 2: - self.stdout.write("Loading '%s' fixtures..." % fixture_name) - else: + # If the fixture we loaded contains 0 objects, assume that an + # error was encountered during fixture loading. + if objects_in_fixture == 0: + raise CommandError( + "No fixture data found for '%s'. " + "(File format may be invalid.)" % fixture_name) + + def _find_fixtures(self, fixture_label): + """ + Finds fixture files for a given label. + """ + fixture_name, ser_fmt, cmp_fmt = self.parse_name(fixture_label) + databases = [self.using, None] + cmp_fmts = list(self.compression_formats.keys()) if cmp_fmt is None else [cmp_fmt] + ser_fmts = serializers.get_public_serializer_formats() if ser_fmt is None else [ser_fmt] + + # Check kept for backwards-compatibility; it doesn't look very useful. + if '.' in os.path.basename(fixture_name): raise CommandError( - "Problem installing fixture '%s': %s is not a known serialization format." % - (fixture_name, format)) - - if os.path.isabs(fixture_name): - fixture_dirs = [fixture_name] - else: - fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + [''] - - for fixture_dir in fixture_dirs: - self.process_dir(fixture_dir, fixture_name, compression_formats, - formats) - - def process_dir(self, fixture_dir, fixture_name, compression_formats, - serialization_formats): - - humanize = lambda dirname: "'%s'" % dirname if dirname else 'absolute path' + "Problem installing fixture '%s': %s is not a known " + "serialization format." % tuple(fixture_name.rsplit('.'))) if self.verbosity >= 2: - self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir)) + self.stdout.write("Loading '%s' fixtures..." % fixture_name) - label_found = False - for combo in product([self.using, None], serialization_formats, compression_formats): - database, format, compression_format = combo - file_name = '.'.join( - p for p in [ - fixture_name, database, format, compression_format - ] - if p - ) + if os.path.isabs(fixture_name): + fixture_dirs = [os.path.dirname(fixture_name)] + fixture_name = os.path.basename(fixture_name) + else: + fixture_dirs = self.fixture_dirs - if self.verbosity >= 3: - 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 = self.compression_types[compression_format] - try: - fixture = open_method(full_path, 'r') - except IOError: - if self.verbosity >= 2: - self.stdout.write("No %s fixture '%s' in %s." % \ - (format, fixture_name, humanize(fixture_dir))) - else: - try: - if label_found: - raise CommandError("Multiple fixtures named '%s' in %s. Aborting." % - (fixture_name, humanize(fixture_dir))) + suffixes = ('.'.join(ext for ext in combo if ext) + for combo in product(databases, ser_fmts, cmp_fmts)) + targets = set('.'.join((fixture_name, suffix)) for suffix in suffixes) - self.fixture_count += 1 - objects_in_fixture = 0 - loaded_objects_in_fixture = 0 - if self.verbosity >= 2: - self.stdout.write("Installing %s fixture '%s' from %s." % \ - (format, fixture_name, humanize(fixture_dir))) + fixture_files = [] + for fixture_dir in fixture_dirs: + if self.verbosity >= 2: + self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir)) + fixture_files_in_dir = [] + for candidate in glob.iglob(os.path.join(fixture_dir, fixture_name + '*')): + if os.path.basename(candidate) in targets: + # Save the fixture_dir and fixture_name for future error messages. + fixture_files_in_dir.append((candidate, fixture_dir, fixture_name)) - objects = serializers.deserialize(format, fixture, using=self.using, ignorenonexistent=self.ignore) + if self.verbosity >= 2 and not fixture_files_in_dir: + self.stdout.write("No fixture '%s' in %s." % + (fixture_name, humanize(fixture_dir))) - for obj in objects: - objects_in_fixture += 1 - if router.allow_syncdb(self.using, obj.object.__class__): - loaded_objects_in_fixture += 1 - self.models.add(obj.object.__class__) - try: - obj.save(using=self.using) - except (DatabaseError, IntegrityError) as e: - e.args = ("Could not load %(app_label)s.%(object_name)s(pk=%(pk)s): %(error_msg)s" % { - 'app_label': obj.object._meta.app_label, - 'object_name': obj.object._meta.object_name, - 'pk': obj.object.pk, - 'error_msg': force_text(e) - },) - raise + # Check kept for backwards-compatibility; it isn't clear why + # duplicates are only allowed in different directories. + if len(fixture_files_in_dir) > 1: + raise CommandError( + "Multiple fixtures named '%s' in %s. Aborting." % + (fixture_name, humanize(fixture_dir))) + fixture_files.extend(fixture_files_in_dir) - self.loaded_object_count += loaded_objects_in_fixture - self.fixture_object_count += objects_in_fixture - label_found = True - except Exception as e: - if not isinstance(e, CommandError): - e.args = ("Problem installing fixture '%s': %s" % (full_path, e),) - raise - finally: - fixture.close() + if fixture_name != 'initial_data' and not fixture_files: + # Warning kept for backwards-compatibility; why not an exception? + warnings.warn("No fixture named '%s' found." % fixture_name) - # If the fixture we loaded contains 0 objects, assume that an - # error was encountered during fixture loading. - if objects_in_fixture == 0: - raise CommandError( - "No fixture data found for '%s'. (File format may be invalid.)" % - (fixture_name)) + return fixture_files + + _label_to_fixtures_cache = {} + find_fixtures = memoize(_find_fixtures, _label_to_fixtures_cache, 2) + + @cached_property + def fixture_dirs(self): + """ + Return a list of fixture directories. + + The list contains the 'fixtures' subdirectory of each installed + application, if it exists, the directories in FIXTURE_DIRS, and the + current directory. + """ + dirs = [] + for path in get_app_paths(): + d = os.path.join(os.path.dirname(path), 'fixtures') + if os.path.isdir(d): + dirs.append(d) + dirs.extend(list(settings.FIXTURE_DIRS)) + dirs.append('') + dirs = [upath(os.path.abspath(os.path.realpath(d))) for d in dirs] + return dirs + + def parse_name(self, fixture_name): + """ + Splits fixture name in name, serialization format, compression format. + """ + parts = fixture_name.rsplit('.', 2) + + if len(parts) > 1 and parts[-1] in self.compression_formats: + cmp_fmt = parts[-1] + parts = parts[:-1] + else: + cmp_fmt = None + + if len(parts) > 1 and parts[-1] in self.serialization_formats: + ser_fmt = parts[-1] + parts = parts[:-1] + else: + ser_fmt = None + + name = '.'.join(parts) + + return name, ser_fmt, cmp_fmt + + +class SingleZipReader(zipfile.ZipFile): + + def __init__(self, *args, **kwargs): + zipfile.ZipFile.__init__(self, *args, **kwargs) + if len(self.namelist()) != 1: + raise ValueError("Zip-compressed fixtures must contain one file.") + + def read(self): + return zipfile.ZipFile.read(self, self.namelist()[0]) + + +def humanize(dirname): + return "'%s'" % dirname if dirname else 'absolute path' diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py index bc171176c2..060def5d5a 100644 --- a/django/core/management/commands/makemessages.py +++ b/django/core/management/commands/makemessages.py @@ -250,18 +250,6 @@ class Command(NoArgsCommand): "if you want to enable i18n for your project or application.") check_programs('xgettext') - # We require gettext version 0.15 or newer. - output, errors, status = popen_wrapper(['xgettext', '--version']) - if status != STATUS_OK: - raise CommandError("Error running xgettext. Note that Django " - "internationalization requires GNU gettext 0.15 or newer.") - match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', output) - if match: - xversion = (int(match.group('major')), int(match.group('minor'))) - if xversion < (0, 15): - raise CommandError("Django internationalization requires GNU " - "gettext 0.15 or newer. You are using version %s, please " - "upgrade your gettext toolset." % match.group()) potfile = self.build_pot_file(localedir) @@ -309,10 +297,9 @@ class Command(NoArgsCommand): """ Check if the given path should be ignored or not. """ - for pattern in ignore_patterns: - if fnmatch.fnmatchcase(path, pattern): - return True - return False + filename = os.path.basename(path) + ignore = lambda pattern: fnmatch.fnmatchcase(filename, pattern) + return any(ignore(pattern) for pattern in ignore_patterns) dir_suffix = '%s*' % os.sep norm_patterns = [p[:-len(dir_suffix)] if p.endswith(dir_suffix) else p for p in self.ignore_patterns] diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 4fe03216d5..c4a0b78cd4 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -99,7 +99,7 @@ class Command(BaseCommand): "started_at": datetime.now().strftime('%B %d, %Y - %X'), "version": self.get_version(), "settings": settings.SETTINGS_MODULE, - "addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr, + "addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr, "port": self.port, "quit_command": quit_command, }) diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index 155c3fb67b..51470d7bda 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -1,11 +1,12 @@ from optparse import make_option +import itertools import traceback from django.conf import settings from django.core.management import call_command from django.core.management.base import NoArgsCommand from django.core.management.color import no_style -from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal +from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.utils.datastructures import SortedDict from django.utils.importlib import import_module @@ -82,6 +83,9 @@ class Command(NoArgsCommand): for app_name, model_list in all_models ) + create_models = set([x for x in itertools.chain(*manifest.values())]) + emit_pre_sync_signal(create_models, verbosity, interactive, db) + # Create the tables for each model if verbosity >= 1: self.stdout.write("Creating tables ...\n") diff --git a/django/core/management/commands/test.py b/django/core/management/commands/test.py index 2b8e8019eb..d6bed7cdb5 100644 --- a/django/core/management/commands/test.py +++ b/django/core/management/commands/test.py @@ -29,7 +29,7 @@ class Command(BaseCommand): ) help = ('Runs the test suite for the specified applications, or the ' 'entire site if no apps are specified.') - args = '[appname ...]' + args = '[appname|appname.tests.TestCase|appname.tests.TestCase.test_method]...' requires_model_validation = False diff --git a/django/core/management/sql.py b/django/core/management/sql.py index ac60ed470c..42ccafa2c5 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -133,14 +133,15 @@ def sql_custom(app, style, connection): def sql_indexes(app, style, connection): "Returns a list of the CREATE INDEX SQL statements for all models in the given app." output = [] - for model in models.get_models(app): + for model in models.get_models(app, include_auto_created=True): output.extend(connection.creation.sql_indexes_for_model(model, style)) return output + def sql_destroy_indexes(app, style, connection): "Returns a list of the DROP INDEX SQL statements for all models in the given app." output = [] - for model in models.get_models(app): + for model in models.get_models(app, include_auto_created=True): output.extend(connection.creation.sql_destroy_indexes_for_model(model, style)) return output @@ -191,6 +192,19 @@ def custom_sql_for_model(model, style, connection): return output +def emit_pre_sync_signal(create_models, verbosity, interactive, db): + # Emit the pre_sync signal for every application. + for app in models.get_apps(): + app_name = app.__name__.split('.')[-2] + if verbosity >= 2: + print("Running pre-sync handlers for application %s" % app_name) + models.signals.pre_syncdb.send(sender=app, app=app, + create_models=create_models, + verbosity=verbosity, + interactive=interactive, + db=db) + + def emit_post_sync_signal(created_models, verbosity, interactive, db): # Emit the post_sync signal for every application. for app in models.get_apps(): diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 0f0eade569..a6d6a76985 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -113,13 +113,15 @@ def get_validation_errors(outfile, app=None): e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name) if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders): e.add(opts, '"%s": FilePathFields must have either allow_files or allow_folders set to True.' % f.name) + if isinstance(f, models.GenericIPAddressField) and not getattr(f, 'null', False) and getattr(f, 'blank', False): + e.add(opts, '"%s": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null.' % f.name) if f.choices: if isinstance(f.choices, six.string_types) or not is_iterable(f.choices): e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name) else: for c in f.choices: - if not isinstance(c, (list, tuple)) or len(c) != 2: - e.add(opts, '"%s": "choices" should be a sequence of two-tuples.' % f.name) + if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2: + e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name) if f.db_index not in (None, True, False): e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name) diff --git a/django/core/paginator.py b/django/core/paginator.py index 9ccff51a34..c8b9377856 100644 --- a/django/core/paginator.py +++ b/django/core/paginator.py @@ -121,7 +121,9 @@ class Page(collections.Sequence): raise TypeError # The object_list is converted to a list so that if it was a QuerySet # it won't be a database hit per __getitem__. - return list(self.object_list)[index] + if not isinstance(self.object_list, list): + self.object_list = list(self.object_list) + return self.object_list[index] def has_next(self): return self.number < self.paginator.num_pages diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index c3d93bb247..d58f2a9fa3 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -75,8 +75,7 @@ class Resolver404(Http404): pass class NoReverseMatch(Exception): - # Don't make this raise an error when used in a template. - silent_variable_failure = True + pass def get_callable(lookup_view, can_fail=False): """ @@ -360,6 +359,9 @@ class RegexURLResolver(LocaleRegexProvider): callback = getattr(urls, 'handler%s' % view_type) return get_callable(callback), {} + def resolve400(self): + return self._resolve_special('400') + def resolve403(self): return self._resolve_special('403') diff --git a/django/core/xheaders.py b/django/core/xheaders.py deleted file mode 100644 index 3766628c98..0000000000 --- a/django/core/xheaders.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -Pages in Django can are served up with custom HTTP headers containing useful -information about those pages -- namely, the content type and object ID. - -This module contains utility functions for retrieving and doing interesting -things with these special "X-Headers" (so called because the HTTP spec demands -that custom headers are prefixed with "X-"). - -Next time you're at slashdot.org, watch out for X-Fry and X-Bender. :) -""" - -def populate_xheaders(request, response, model, object_id): - """ - Adds the "X-Object-Type" and "X-Object-Id" headers to the given - HttpResponse according to the given model and object_id -- but only if the - given HttpRequest object has an IP address within the INTERNAL_IPS setting - or if the request is from a logged in staff member. - """ - from django.conf import settings - if (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS - or (hasattr(request, 'user') and request.user.is_active - and request.user.is_staff)): - response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.model_name) - response['X-Object-Id'] = str(object_id) diff --git a/django/db/__init__.py b/django/db/__init__.py index 08c901ab7b..2421ddeab8 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -1,24 +1,19 @@ import warnings -from django.conf import settings from django.core import signals -from django.core.exceptions import ImproperlyConfigured from django.db.utils import (DEFAULT_DB_ALIAS, DataError, OperationalError, IntegrityError, InternalError, ProgrammingError, NotSupportedError, DatabaseError, InterfaceError, Error, load_backend, ConnectionHandler, ConnectionRouter) +from django.utils.functional import cached_property __all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError', 'IntegrityError', 'DEFAULT_DB_ALIAS') +connections = ConnectionHandler() -if settings.DATABASES and DEFAULT_DB_ALIAS not in settings.DATABASES: - raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS) - -connections = ConnectionHandler(settings.DATABASES) - -router = ConnectionRouter(settings.DATABASE_ROUTERS) +router = ConnectionRouter() # `connection`, `DatabaseError` and `IntegrityError` are convenient aliases # for backend bits. @@ -45,7 +40,28 @@ class DefaultConnectionProxy(object): return delattr(connections[DEFAULT_DB_ALIAS], name) connection = DefaultConnectionProxy() -backend = load_backend(connection.settings_dict['ENGINE']) + +class DefaultBackendProxy(object): + """ + Temporary proxy class used during deprecation period of the `backend` module + variable. + """ + @cached_property + def _backend(self): + warnings.warn("Accessing django.db.backend is deprecated.", + PendingDeprecationWarning, stacklevel=2) + return load_backend(connections[DEFAULT_DB_ALIAS].settings_dict['ENGINE']) + + def __getattr__(self, item): + return getattr(self._backend, item) + + def __setattr__(self, name, value): + return setattr(self._backend, name, value) + + def __delattr__(self, name): + return delattr(self._backend, name) + +backend = DefaultBackendProxy() def close_connection(**kwargs): warnings.warn( diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 0b9e75cbbc..1b8e6ae447 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -390,9 +390,10 @@ class BaseDatabaseWrapper(object): def disable_constraint_checking(self): """ Backends can implement as needed to temporarily disable foreign key - constraint checking. + constraint checking. Should return True if the constraints were + disabled and will need to be reenabled. """ - pass + return False def enable_constraint_checking(self): """ @@ -784,12 +785,12 @@ class BaseDatabaseOperations(object): """ return cursor.fetchone()[0] - def field_cast_sql(self, db_type): + def field_cast_sql(self, db_type, internal_type): """ - Given a column type (e.g. 'BLOB', 'VARCHAR'), returns the SQL necessary - to cast it before using it in a WHERE statement. Note that the - resulting string should contain a '%s' placeholder for the column being - searched against. + Given a column type (e.g. 'BLOB', 'VARCHAR'), and an internal type + (e.g. 'GenericIPAddressField'), returns the SQL necessary to cast it + before using it in a WHERE statement. Note that the resulting string + should contain a '%s' placeholder for the column being searched against. """ return '%s' diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 21cea1fef8..98830407fb 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -99,7 +99,7 @@ class BaseDatabaseCreation(object): style.SQL_TABLE(qn(opts.db_table)) + ' ('] for i, line in enumerate(table_output): # Combine and add commas. full_statement.append( - ' %s%s' % (line, i < len(table_output) - 1 and ',' or '')) + ' %s%s' % (line, ',' if i < len(table_output) - 1 else '')) full_statement.append(')') if opts.db_tablespace: tablespace_sql = self.connection.ops.tablespace_sql( diff --git a/django/db/backends/mysql/compiler.py b/django/db/backends/mysql/compiler.py index 50a085212b..4e033e3d93 100644 --- a/django/db/backends/mysql/compiler.py +++ b/django/db/backends/mysql/compiler.py @@ -17,8 +17,7 @@ class SQLCompiler(compiler.SQLCompiler): values.append(value) return row[:index_extra_select] + tuple(values) - def as_subquery_condition(self, alias, columns): - qn = self.quote_name_unless_alias + def as_subquery_condition(self, alias, columns, qn): qn2 = self.connection.ops.quote_name sql, params = self.as_sql() return '(%s) IN (%s)' % (', '.join(['%s.%s' % (qn(alias), qn2(column)) for column in columns]), sql), params diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 66860d3d01..798c735d7b 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -44,6 +44,11 @@ except ImportError as e: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e) +try: + import pytz +except ImportError: + pytz = None + from django.db import utils from django.db.backends import * from django.db.backends.oracle.client import DatabaseClient @@ -78,6 +83,7 @@ class DatabaseFeatures(BaseDatabaseFeatures): supports_subqueries_in_group_by = False supports_transactions = True supports_timezones = False + has_zoneinfo_database = pytz is not None supports_bitwise_or = False can_defer_constraint_checks = True ignores_nulls_in_unique_constraints = False @@ -243,9 +249,6 @@ WHEN (new.%(col_name)s IS NULL) value = value.date() return value - def datetime_cast_sql(self): - return "TO_TIMESTAMP(%s, 'YYYY-MM-DD HH24:MI:SS.FF')" - def deferrable_sql(self): return " DEFERRABLE INITIALLY DEFERRED" @@ -255,7 +258,7 @@ WHEN (new.%(col_name)s IS NULL) def fetch_returned_insert_id(self, cursor): return int(cursor._insert_id_var.getvalue()) - def field_cast_sql(self, db_type): + def field_cast_sql(self, db_type, internal_type): if db_type and db_type.endswith('LOB'): return "DBMS_LOB.SUBSTR(%s)" else: @@ -434,6 +437,17 @@ WHEN (new.%(col_name)s IS NULL) second = '%s-12-31' return [first % value, second % value] + def year_lookup_bounds_for_datetime_field(self, value): + # The default implementation uses datetime objects for the bounds. + # This must be overridden here, to use a formatted date (string) as + # 'second' instead -- cx_Oracle chops the fraction-of-second part + # off of datetime objects, leaving almost an entire second out of + # the year under the default implementation. + bounds = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value) + if settings.USE_TZ: + bounds = [b.astimezone(timezone.utc).replace(tzinfo=None) for b in bounds] + return [b.isoformat(b' ') for b in bounds] + def combine_expression(self, connector, sub_expressions): "Oracle requires special cases for %% and & operators in query expressions" if connector == '%%': diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index c8a436c8cd..7f1192ed8a 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -1,5 +1,7 @@ import sys import time + +from django.conf import settings from django.db.backends.creation import BaseDatabaseCreation from django.utils.six.moves import input @@ -112,7 +114,6 @@ class DatabaseCreation(BaseDatabaseCreation): print("Tests cancelled.") sys.exit(1) - from django.db import settings real_settings = settings.DATABASES[self.connection.alias] real_settings['SAVED_USER'] = self.connection.settings_dict['SAVED_USER'] = self.connection.settings_dict['USER'] real_settings['SAVED_PASSWORD'] = self.connection.settings_dict['SAVED_PASSWORD'] = self.connection.settings_dict['PASSWORD'] diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index ff56dca5c2..361308a62c 100644 --- a/django/db/backends/oracle/introspection.py +++ b/django/db/backends/oracle/introspection.py @@ -1,4 +1,5 @@ from django.db.backends import BaseDatabaseIntrospection, FieldInfo +from django.utils.encoding import force_text import cx_Oracle import re @@ -48,7 +49,9 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): cursor.execute("SELECT * FROM %s WHERE ROWNUM < 2" % self.connection.ops.quote_name(table_name)) description = [] for desc in cursor.description: - description.append(FieldInfo(*((desc[0].lower(),) + desc[1:]))) + name = force_text(desc[0]) # cx_Oracle always returns a 'str' on both Python 2 and 3 + name = name % {} # cx_Oracle, for some reason, doubles percent signs. + description.append(FieldInfo(*(name.lower(),) + desc[1:])) return description def table_name_converter(self, name): @@ -87,6 +90,18 @@ class DatabaseIntrospection(BaseDatabaseIntrospection): relations[row[0]] = (row[2], row[1].lower()) return relations + def get_key_columns(self, cursor, table_name): + cursor.execute(""" + SELECT ccol.column_name, rcol.table_name AS referenced_table, rcol.column_name AS referenced_column + FROM user_constraints c + JOIN user_cons_columns ccol + ON ccol.constraint_name = c.constraint_name + JOIN user_cons_columns rcol + ON rcol.constraint_name = c.r_constraint_name + WHERE c.table_name = %s AND c.constraint_type = 'R'""" , [table_name.upper()]) + return [tuple(cell.lower() for cell in row) + for row in cursor.fetchall()] + def get_indexes(self, cursor, table_name): sql = """ SELECT LOWER(uic1.column_name) AS column_name, diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index b17a0c17bb..f06eec5a1d 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -78,8 +78,8 @@ class DatabaseOperations(BaseDatabaseOperations): return lookup - def field_cast_sql(self, db_type): - if db_type == 'inet': + def field_cast_sql(self, db_type, internal_type): + if internal_type == "GenericIPAddressField" or internal_type == "IPAddressField": return 'HOST(%s)' return '%s' diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 084f4c200b..aa2601277a 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -83,7 +83,7 @@ class CursorDebugWrapper(CursorWrapper): ############################################### def typecast_date(s): - return s and datetime.date(*map(int, s.split('-'))) or None # returns None if s is null + return datetime.date(*map(int, s.split('-'))) if s else None # returns None if s is null def typecast_time(s): # does NOT store time zone information if not s: return None diff --git a/django/db/models/__init__.py b/django/db/models/__init__.py index 5f17229753..3eac2167d4 100644 --- a/django/db/models/__init__.py +++ b/django/db/models/__init__.py @@ -1,7 +1,7 @@ from functools import wraps from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured -from django.db.models.loading import get_apps, get_app, get_models, get_model, register_models +from django.db.models.loading import get_apps, get_app_paths, get_app, get_models, get_model, register_models from django.db.models.query import Q from django.db.models.expressions import F from django.db.models.manager import Manager diff --git a/django/db/models/base.py b/django/db/models/base.py index 556249fa54..5f1c21c255 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -632,15 +632,7 @@ class Model(six.with_metaclass(ModelBase)): base_qs = cls._base_manager.using(using) values = [(f, None, (getattr(self, f.attname) if raw else f.pre_save(self, False))) for f in non_pks] - if not values: - # We can end up here when saving a model in inheritance chain where - # update_fields doesn't target any field in current model. In that - # case we just say the update succeeded. Another case ending up here - # is a model with just PK - in that case check that the PK still - # exists. - updated = update_fields is not None or base_qs.filter(pk=pk_val).exists() - else: - updated = self._do_update(base_qs, using, pk_val, values) + updated = self._do_update(base_qs, using, pk_val, values, update_fields) if force_update and not updated: raise DatabaseError("Forced update did not affect any rows.") if update_fields and not updated: @@ -664,13 +656,21 @@ class Model(six.with_metaclass(ModelBase)): setattr(self, meta.pk.attname, result) return updated - def _do_update(self, base_qs, using, pk_val, values): + def _do_update(self, base_qs, using, pk_val, values, update_fields): """ This method will try to update the model. If the model was updated (in the sense that an update query was done and a matching row was found from the DB) the method will return True. """ - return base_qs.filter(pk=pk_val)._update(values) > 0 + if not values: + # We can end up here when saving a model in inheritance chain where + # update_fields doesn't target any field in current model. In that + # case we just say the update succeeded. Another case ending up here + # is a model with just PK - in that case check that the PK still + # exists. + return update_fields is not None or base_qs.filter(pk=pk_val).exists() + else: + return base_qs.filter(pk=pk_val)._update(values) > 0 def _do_insert(self, manager, using, fields, update_pk, raw): """ diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 86a0711d7c..691eeffb08 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -243,6 +243,8 @@ class Field(object): obj = copy.copy(self) if self.rel: obj.rel = copy.copy(self.rel) + if hasattr(self.rel, 'field') and self.rel.field is self: + obj.rel.field = obj memodict[id(self)] = obj return obj diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 5ef713e5e6..754a97633b 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -11,7 +11,7 @@ from django.db.models.deletion import CASCADE from django.utils.encoding import smart_text from django.utils import six from django.utils.deprecation import RenameMethodsBase -from django.utils.translation import ugettext_lazy as _, string_concat +from django.utils.translation import ugettext_lazy as _ from django.utils.functional import curry, cached_property from django.core import exceptions from django import forms @@ -199,7 +199,9 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri setattr(rel_obj, self.related.field.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) if rel_obj is None: - raise self.related.model.DoesNotExist + raise self.related.model.DoesNotExist("%s has no %s." % ( + instance.__class__.__name__, + self.related.get_accessor_name())) else: return rel_obj @@ -224,8 +226,7 @@ class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescri value._state.db = router.db_for_write(value.__class__, instance=instance) elif value._state.db is not None and instance._state.db is not None: if not router.allow_relation(value, instance): - raise ValueError('Cannot assign "%r": instance is on database "%s", value is on database "%s"' % - (value, instance._state.db, value._state.db)) + raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value) related_pk = tuple([getattr(instance, field.attname) for field in self.related.field.foreign_related_fields]) if None in related_pk: @@ -302,7 +303,8 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec setattr(rel_obj, self.field.related.get_cache_name(), instance) setattr(instance, self.cache_name, rel_obj) if rel_obj is None and not self.field.null: - raise self.field.rel.to.DoesNotExist + raise self.field.rel.to.DoesNotExist( + "%s has no %s." % (self.field.model.__name__, self.field.name)) else: return rel_obj @@ -323,8 +325,7 @@ class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjec value._state.db = router.db_for_write(value.__class__, instance=instance) elif value._state.db is not None and instance._state.db is not None: if not router.allow_relation(value, instance): - raise ValueError('Cannot assign "%r": instance is on database "%s", value is on database "%s"' % - (value, instance._state.db, value._state.db)) + raise ValueError('Cannot assign "%r": the current database router prevents this relation.' % value) # If we're setting the value of a OneToOneField to None, we need to clear # out the cache on any old related object. Otherwise, deleting the @@ -1379,9 +1380,6 @@ class ManyToManyField(RelatedField): super(ManyToManyField, self).__init__(**kwargs) - msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') - self.help_text = string_concat(self.help_text, ' ', msg) - def deconstruct(self): name, path, args, kwargs = super(ManyToManyField, self).deconstruct() # Handle the simpler arguments diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 075cae4c61..535df7ce80 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -152,7 +152,9 @@ class BaseAppCache(object): return self.loaded def get_apps(self): - "Returns a list of all installed modules that contain models." + """ + Returns a list of all installed modules that contain models. + """ self._populate() # Ensure the returned list is always in the same order (with new apps @@ -162,6 +164,23 @@ class BaseAppCache(object): apps.sort() return [elt[1] for elt in apps] + def get_app_paths(self): + """ + Returns a list of paths to all installed apps. + + Useful for discovering files at conventional locations inside apps + (static files, templates, etc.) + """ + self._populate() + + app_paths = [] + for app in self.get_apps(): + if hasattr(app, '__path__'): # models/__init__.py package + app_paths.extend([upath(path) for path in app.__path__]) + else: # models.py module + app_paths.append(upath(app.__file__)) + return app_paths + def get_app(self, app_label, emptyOK=False): """ Returns the module containing the models for the given app_label. If @@ -302,6 +321,7 @@ cache = AppCache() # These methods were always module level, so are kept that way for backwards # compatibility. get_apps = cache.get_apps +get_app_paths = cache.get_app_paths get_app = cache.get_app get_app_errors = cache.get_app_errors get_models = cache.get_models diff --git a/django/db/models/manager.py b/django/db/models/manager.py index 43a8264f11..a1aa79f809 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -186,6 +186,12 @@ class Manager(six.with_metaclass(RenameManagerMethods)): def latest(self, *args, **kwargs): return self.get_queryset().latest(*args, **kwargs) + def first(self): + return self.get_queryset().first() + + def last(self): + return self.get_queryset().last() + def order_by(self, *args, **kwargs): return self.get_queryset().order_by(*args, **kwargs) diff --git a/django/db/models/query.py b/django/db/models/query.py index d3763d3934..b0ce25f5b5 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -9,7 +9,7 @@ import warnings from django.conf import settings from django.core import exceptions -from django.db import connections, router, transaction, IntegrityError +from django.db import connections, router, transaction, DatabaseError from django.db.models.constants import LOOKUP_SEP from django.db.models.fields import AutoField from django.db.models.query_utils import (Q, select_related_descend, @@ -20,11 +20,6 @@ from django.utils.functional import partition from django.utils import six from django.utils import timezone -# Used to control how many objects are worked with at once in some cases (e.g. -# when deleting objects). -CHUNK_SIZE = 100 -ITER_CHUNK_SIZE = CHUNK_SIZE - # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 @@ -41,7 +36,6 @@ class QuerySet(object): self._db = using self.query = query or sql.Query(self.model) self._result_cache = None - self._iter = None self._sticky_filter = False self._for_write = False self._prefetch_related_lookups = [] @@ -57,8 +51,8 @@ class QuerySet(object): Deep copy of a QuerySet doesn't populate the cache """ obj = self.__class__() - for k,v in self.__dict__.items(): - if k in ('_iter','_result_cache'): + for k, v in self.__dict__.items(): + if k == '_result_cache': obj.__dict__[k] = None else: obj.__dict__[k] = copy.deepcopy(v, memo) @@ -69,10 +63,8 @@ class QuerySet(object): Allows the QuerySet to be pickled. """ # Force the cache to be fully populated. - len(self) - + self._fetch_all() obj_dict = self.__dict__.copy() - obj_dict['_iter'] = None return obj_dict def __repr__(self): @@ -82,95 +74,31 @@ class QuerySet(object): return repr(data) def __len__(self): - # Since __len__ is called quite frequently (for example, as part of - # list(qs), we make some effort here to be as efficient as possible - # whilst not messing up any existing iterators against the QuerySet. - if self._result_cache is None: - if self._iter: - self._result_cache = list(self._iter) - else: - self._result_cache = list(self.iterator()) - elif self._iter: - self._result_cache.extend(self._iter) - if self._prefetch_related_lookups and not self._prefetch_done: - self._prefetch_related_objects() + self._fetch_all() return len(self._result_cache) def __iter__(self): - if self._prefetch_related_lookups and not self._prefetch_done: - # We need all the results in order to be able to do the prefetch - # in one go. To minimize code duplication, we use the __len__ - # code path which also forces this, and also does the prefetch - len(self) - - if self._result_cache is None: - self._iter = self.iterator() - self._result_cache = [] - if self._iter: - return self._result_iter() - # Python's list iterator is better than our version when we're just - # iterating over the cache. + """ + The queryset iterator protocol uses three nested iterators in the + default case: + 1. sql.compiler:execute_sql() + - Returns 100 rows at time (constants.GET_ITERATOR_CHUNK_SIZE) + using cursor.fetchmany(). This part is responsible for + doing some column masking, and returning the rows in chunks. + 2. sql/compiler.results_iter() + - Returns one row at time. At this point the rows are still just + tuples. In some cases the return values are converted to + Python values at this location (see resolve_columns(), + resolve_aggregate()). + 3. self.iterator() + - Responsible for turning the rows into model objects. + """ + self._fetch_all() return iter(self._result_cache) - def _result_iter(self): - pos = 0 - while 1: - upper = len(self._result_cache) - while pos < upper: - yield self._result_cache[pos] - pos = pos + 1 - if not self._iter: - raise StopIteration - if len(self._result_cache) <= pos: - self._fill_cache() - - def __bool__(self): - if self._prefetch_related_lookups and not self._prefetch_done: - # We need all the results in order to be able to do the prefetch - # in one go. To minimize code duplication, we use the __len__ - # code path which also forces this, and also does the prefetch - len(self) - - if self._result_cache is not None: - return bool(self._result_cache) - try: - next(iter(self)) - except StopIteration: - return False - return True - - def __nonzero__(self): # Python 2 compatibility - return type(self).__bool__(self) - - def __contains__(self, val): - # The 'in' operator works without this method, due to __iter__. This - # implementation exists only to shortcut the creation of Model - # instances, by bailing out early if we find a matching element. - pos = 0 - if self._result_cache is not None: - if val in self._result_cache: - return True - elif self._iter is None: - # iterator is exhausted, so we have our answer - return False - # remember not to check these again: - pos = len(self._result_cache) - else: - # We need to start filling the result cache out. The following - # ensures that self._iter is not None and self._result_cache is not - # None - it = iter(self) - - # Carry on, one result at a time. - while True: - if len(self._result_cache) <= pos: - self._fill_cache(num=1) - if self._iter is None: - # we ran out of items - return False - if self._result_cache[pos] == val: - return True - pos += 1 + def __nonzero__(self): + self._fetch_all() + return bool(self._result_cache) def __getitem__(self, k): """ @@ -184,19 +112,6 @@ class QuerySet(object): "Negative indexing is not supported." if self._result_cache is not None: - if self._iter is not None: - # The result cache has only been partially populated, so we may - # need to fill it out a bit more. - if isinstance(k, slice): - if k.stop is not None: - # Some people insist on passing in strings here. - bound = int(k.stop) - else: - bound = None - else: - bound = k + 1 - if len(self._result_cache) < bound: - self._fill_cache(bound - len(self._result_cache)) return self._result_cache[k] if isinstance(k, slice): @@ -210,7 +125,7 @@ class QuerySet(object): else: stop = None qs.query.set_limits(start, stop) - return k.step and list(qs)[::k.step] or qs + return list(qs)[::k.step] if k.step else qs qs = self._clone() qs.query.set_limits(k, k + 1) @@ -370,7 +285,7 @@ class QuerySet(object): If the QuerySet is already fully cached this simply returns the length of the cached results set to avoid multiple SELECT COUNT(*) calls. """ - if self._result_cache is not None and not self._iter: + if self._result_cache is not None: return len(self._result_cache) return self.query.get_count(using=self.db) @@ -388,13 +303,11 @@ class QuerySet(object): return clone._result_cache[0] if not num: raise self.model.DoesNotExist( - "%s matching query does not exist. " - "Lookup parameters were %s" % - (self.model._meta.object_name, kwargs)) + "%s matching query does not exist." % + self.model._meta.object_name) raise self.model.MultipleObjectsReturned( - "get() returned more than one %s -- it returned %s! " - "Lookup parameters were %s" % - (self.model._meta.object_name, num, kwargs)) + "get() returned more than one %s -- it returned %s!" % + (self.model._meta.object_name, num)) def create(self, **kwargs): """ @@ -450,8 +363,6 @@ class QuerySet(object): Returns a tuple of (object, created), where created is a boolean specifying whether an object was created. """ - assert kwargs, \ - 'get_or_create() must be passed at least one keyword argument' defaults = kwargs.pop('defaults', {}) lookup = kwargs.copy() for f in self.model._meta.fields: @@ -469,13 +380,13 @@ class QuerySet(object): obj.save(force_insert=True, using=self.db) transaction.savepoint_commit(sid, using=self.db) return obj, True - except IntegrityError: + except DatabaseError: transaction.savepoint_rollback(sid, using=self.db) exc_info = sys.exc_info() try: return self.get(**lookup), False except self.model.DoesNotExist: - # Re-raise the IntegrityError with its original traceback. + # Re-raise the DatabaseError with its original traceback. six.reraise(*exc_info) def _earliest_or_latest(self, field_name=None, direction="-"): @@ -500,6 +411,26 @@ class QuerySet(object): def latest(self, field_name=None): return self._earliest_or_latest(field_name=field_name, direction="-") + def first(self): + """ + Returns the first object of a query, returns None if no match is found. + """ + qs = self if self.ordered else self.order_by('pk') + try: + return qs[0] + except IndexError: + return None + + def last(self): + """ + Returns the last object of a query, returns None if no match is found. + """ + qs = self.reverse() if self.ordered else self.order_by('-pk') + try: + return qs[0] + except IndexError: + return None + def in_bulk(self, id_list): """ Returns a dictionary mapping each of the given IDs to the object with @@ -714,6 +645,8 @@ class QuerySet(object): If fields are specified, they must be ForeignKey fields and only those related objects are included in the selection. + + If select_related(None) is called, the list is cleared. """ if 'depth' in kwargs: warnings.warn('The "depth" keyword argument has been deprecated.\n' @@ -723,7 +656,9 @@ class QuerySet(object): raise TypeError('Unexpected keyword arguments to select_related: %s' % (list(kwargs),)) obj = self._clone() - if fields: + if fields == (None,): + obj.query.select_related = False + elif fields: if depth: raise TypeError('Cannot pass both "depth" and fields to select_related()') obj.query.add_select_related(fields) @@ -915,17 +850,11 @@ class QuerySet(object): c._setup_query() return c - def _fill_cache(self, num=None): - """ - Fills the result cache with 'num' more entries (or until the results - iterator is exhausted). - """ - if self._iter: - try: - for i in range(num or ITER_CHUNK_SIZE): - self._result_cache.append(next(self._iter)) - except StopIteration: - self._iter = None + def _fetch_all(self): + if self._result_cache is None: + self._result_cache = list(self.iterator()) + if self._prefetch_related_lookups and not self._prefetch_done: + self._prefetch_related_objects() def _next_is_sticky(self): """ @@ -1618,8 +1547,18 @@ def prefetch_related_objects(result_cache, related_lookups): if len(obj_list) == 0: break + current_lookup = LOOKUP_SEP.join(attrs[0:level+1]) + if current_lookup in done_queries: + # Skip any prefetching, and any object preparation + obj_list = done_queries[current_lookup] + continue + + # Prepare objects: good_objects = True for obj in obj_list: + # Since prefetching can re-use instances, it is possible to have + # the same instance multiple times in obj_list, so obj might + # already be prepared. if not hasattr(obj, '_prefetched_objects_cache'): try: obj._prefetched_objects_cache = {} @@ -1630,9 +1569,6 @@ def prefetch_related_objects(result_cache, related_lookups): # now. good_objects = False break - else: - # We already did this list - break if not good_objects: break @@ -1657,23 +1593,18 @@ def prefetch_related_objects(result_cache, related_lookups): "prefetch_related()." % lookup) if prefetcher is not None and not is_fetched: - # Check we didn't do this already - current_lookup = LOOKUP_SEP.join(attrs[0:level+1]) - if current_lookup in done_queries: - obj_list = done_queries[current_lookup] - else: - obj_list, additional_prl = prefetch_one_level(obj_list, prefetcher, attr) - # We need to ensure we don't keep adding lookups from the - # same relationships to stop infinite recursion. So, if we - # are already on an automatically added lookup, don't add - # the new lookups from relationships we've seen already. - if not (lookup in auto_lookups and - descriptor in followed_descriptors): - for f in additional_prl: - new_prl = LOOKUP_SEP.join([current_lookup, f]) - auto_lookups.append(new_prl) - done_queries[current_lookup] = obj_list - followed_descriptors.add(descriptor) + obj_list, additional_prl = prefetch_one_level(obj_list, prefetcher, attr) + # We need to ensure we don't keep adding lookups from the + # same relationships to stop infinite recursion. So, if we + # are already on an automatically added lookup, don't add + # the new lookups from relationships we've seen already. + if not (lookup in auto_lookups and + descriptor in followed_descriptors): + for f in additional_prl: + new_prl = LOOKUP_SEP.join([current_lookup, f]) + auto_lookups.append(new_prl) + done_queries[current_lookup] = obj_list + followed_descriptors.add(descriptor) else: # Either a singly related object that has already been fetched # (e.g. via select_related), or hopefully some other property diff --git a/django/db/models/signals.py b/django/db/models/signals.py index 09f93d0f77..3e321893c1 100644 --- a/django/db/models/signals.py +++ b/django/db/models/signals.py @@ -12,6 +12,7 @@ post_save = Signal(providing_args=["instance", "raw", "created", "using", "updat pre_delete = Signal(providing_args=["instance", "using"], use_caching=True) post_delete = Signal(providing_args=["instance", "using"], use_caching=True) +pre_syncdb = Signal(providing_args=["app", "create_models", "verbosity", "interactive", "db"]) post_syncdb = Signal(providing_args=["class", "app", "created_models", "verbosity", "interactive", "db"], use_caching=True) m2m_changed = Signal(providing_args=["action", "instance", "reverse", "model", "pk_set", "using"], use_caching=True) diff --git a/django/db/models/sql/aggregates.py b/django/db/models/sql/aggregates.py index 23b79923d1..2bd2b2f76f 100644 --- a/django/db/models/sql/aggregates.py +++ b/django/db/models/sql/aggregates.py @@ -99,7 +99,7 @@ class Count(Aggregate): sql_template = '%(function)s(%(distinct)s%(field)s)' def __init__(self, col, distinct=False, **extra): - super(Count, self).__init__(col, distinct=distinct and 'DISTINCT ' or '', **extra) + super(Count, self).__init__(col, distinct='DISTINCT ' if distinct else '', **extra) class Max(Aggregate): sql_function = 'MAX' diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index bbe310c8c3..0bfd1b38d3 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -729,7 +729,8 @@ class SQLCompiler(object): row = self.resolve_columns(row, fields) if has_aggregate_select: - aggregate_start = len(self.query.extra_select) + len(self.query.select) + loaded_fields = self.query.get_loaded_field_names().get(self.query.model, set()) or self.query.select + aggregate_start = len(self.query.extra_select) + len(loaded_fields) aggregate_end = aggregate_start + len(self.query.aggregate_select) row = tuple(row[:aggregate_start]) + tuple([ self.query.resolve_aggregate(value, aggregate, self.connection) @@ -786,8 +787,7 @@ class SQLCompiler(object): return list(result) return result - def as_subquery_condition(self, alias, columns): - qn = self.quote_name_unless_alias + def as_subquery_condition(self, alias, columns, qn): qn2 = self.connection.ops.quote_name if len(columns) == 1: sql, params = self.as_sql() diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 0a4152587d..154b6bd204 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -1422,7 +1422,9 @@ class Query(object): query.clear_ordering(True) # Try to have as simple as possible subquery -> trim leading joins from # the subquery. - trimmed_joins = query.trim_start(names_with_path) + trimmed_prefix, contains_louter = query.trim_start(names_with_path) + query.remove_inherited_models() + # Add extra check to make sure the selected field will not be null # since we are adding a IN <subquery> clause. This prevents the # database from tripping over IN (...,NULL,...) selects and returning @@ -1431,38 +1433,20 @@ class Query(object): alias, col = query.select[0].col query.where.add((Constraint(alias, col, query.select[0].field), 'isnull', False), AND) - # Still make sure that the trimmed parts in the inner query and - # trimmed prefix are in sync. So, use the trimmed_joins to make sure - # as many path elements are in the prefix as there were trimmed joins. - # In addition, convert the path elements back to names so that - # add_filter() can handle them. - trimmed_prefix = [] - paths_in_prefix = trimmed_joins - for name, path in names_with_path: - if paths_in_prefix - len(path) < 0: - break - trimmed_prefix.append(name) - paths_in_prefix -= len(path) - join_field = path[paths_in_prefix].join_field - # TODO: This should be made properly multicolumn - # join aware. It is likely better to not use build_filter - # at all, instead construct joins up to the correct point, - # then construct the needed equality constraint manually, - # or maybe using SubqueryConstraint would work, too. - # The foreign_related_fields attribute is right here, we - # don't ever split joins for direct case. - trimmed_prefix.append( - join_field.field.foreign_related_fields[0].name) - trimmed_prefix = LOOKUP_SEP.join(trimmed_prefix) condition = self.build_filter( ('%s__in' % trimmed_prefix, query), current_negated=True, branch_negated=True, can_reuse=can_reuse) - # Intentionally leave the other alias as blank, if the condition - # refers it, things will break here. - extra_restriction = join_field.get_extra_restriction( - self.where_class, None, [t for t in query.tables if query.alias_refcount[t]][0]) - if extra_restriction: - query.where.add(extra_restriction, 'AND') + if contains_louter: + or_null_condition = self.build_filter( + ('%s__isnull' % trimmed_prefix, True), + current_negated=True, branch_negated=True, can_reuse=can_reuse) + condition.add(or_null_condition, OR) + # Note that the end result will be: + # (outercol NOT IN innerq AND outercol IS NOT NULL) OR outercol IS NULL. + # This might look crazy but due to how IN works, this seems to be + # correct. If the IS NOT NULL check is removed then outercol NOT + # IN will return UNKNOWN. If the IS NULL check is removed, then if + # outercol IS NULL we will not match the row. return condition def set_empty(self): @@ -1821,35 +1805,58 @@ class Query(object): def trim_start(self, names_with_path): """ Trims joins from the start of the join path. The candidates for trim - are the PathInfos in names_with_path structure. Outer joins are not - eligible for removal. Also sets the select column so the start - matches the join. + are the PathInfos in names_with_path structure that are m2m joins. - This method is mostly useful for generating the subquery joins & col - in "WHERE somecol IN (subquery)". This construct is needed by - split_exclude(). + Also sets the select column so the start matches the join. + + This method is meant to be used for generating the subquery joins & + cols in split_exclude(). + + Returns a lookup usable for doing outerq.filter(lookup=self). Returns + also if the joins in the prefix contain a LEFT OUTER join. _""" all_paths = [] for _, paths in names_with_path: all_paths.extend(paths) - direct_join = True + contains_louter = False for pos, path in enumerate(all_paths): - if self.alias_map[self.tables[pos + 1]].join_type == self.LOUTER: - direct_join = False - pos -= 1 + if path.m2m: break + if self.alias_map[self.tables[pos + 1]].join_type == self.LOUTER: + contains_louter = True self.unref_alias(self.tables[pos]) - if path.direct: - direct_join = not direct_join - join_side = 0 if direct_join else 1 - select_alias = self.tables[pos + 1] - join_field = path.join_field - if hasattr(join_field, 'field'): - join_field = join_field.field - select_fields = [r[join_side] for r in join_field.related_fields] + # The path.join_field is a Rel, lets get the other side's field + join_field = path.join_field.field + # Build the filter prefix. + trimmed_prefix = [] + paths_in_prefix = pos + for name, path in names_with_path: + if paths_in_prefix - len(path) < 0: + break + trimmed_prefix.append(name) + paths_in_prefix -= len(path) + trimmed_prefix.append( + join_field.foreign_related_fields[0].name) + trimmed_prefix = LOOKUP_SEP.join(trimmed_prefix) + # Lets still see if we can trim the first join from the inner query + # (that is, self). We can't do this for LEFT JOINs because we would + # miss those rows that have nothing on the outer side. + if self.alias_map[self.tables[pos + 1]].join_type != self.LOUTER: + select_fields = [r[0] for r in join_field.related_fields] + select_alias = self.tables[pos + 1] + self.unref_alias(self.tables[pos]) + extra_restriction = join_field.get_extra_restriction( + self.where_class, None, self.tables[pos + 1]) + if extra_restriction: + self.where.add(extra_restriction, AND) + else: + # TODO: It might be possible to trim more joins from the start of the + # inner query if it happens to have a longer join chain containing the + # values in select_fields. Lets punt this one for now. + select_fields = [r[1] for r in join_field.related_fields] + select_alias = self.tables[pos] self.select = [SelectInfo((select_alias, f.column), f) for f in select_fields] - self.remove_inherited_models() - return pos + return trimmed_prefix, contains_louter def is_nullable(self, field): """ diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 029226383d..2a342d417a 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -174,6 +174,8 @@ class WhereNode(tree.Node): it. """ lvalue, lookup_type, value_annotation, params_or_value = child + field_internal_type = lvalue.field.get_internal_type() if lvalue.field else None + if isinstance(lvalue, Constraint): try: lvalue, params = lvalue.process(lookup_type, params_or_value, connection) @@ -187,7 +189,7 @@ class WhereNode(tree.Node): if isinstance(lvalue, tuple): # A direct database column lookup. - field_sql, field_params = self.sql_for_columns(lvalue, qn, connection), [] + field_sql, field_params = self.sql_for_columns(lvalue, qn, connection, field_internal_type), [] else: # A smart object with an as_sql() method. field_sql, field_params = lvalue.as_sql(qn, connection) @@ -257,7 +259,7 @@ class WhereNode(tree.Node): raise TypeError('Invalid lookup_type: %r' % lookup_type) - def sql_for_columns(self, data, qn, connection): + def sql_for_columns(self, data, qn, connection, internal_type=None): """ Returns the SQL fragment used for the left-hand side of a column constraint (for example, the "T1.foo" portion in the clause @@ -268,7 +270,7 @@ class WhereNode(tree.Node): lhs = '%s.%s' % (qn(table_alias), qn(name)) else: lhs = qn(name) - return connection.ops.field_cast_sql(db_type) % lhs + return connection.ops.field_cast_sql(db_type, internal_type) % lhs def relabel_aliases(self, change_map): """ @@ -397,13 +399,21 @@ class SubqueryConstraint(object): if hasattr(query, 'values'): if query._db and connection.alias != query._db: raise ValueError("Can't do subqueries with queries on different DBs.") - query = query.values(*self.targets).query + # Do not override already existing values. + if not hasattr(query, 'field_names'): + query = query.values(*self.targets) + else: + query = query._clone() + query = query.query query.clear_ordering(True) query_compiler = query.get_compiler(connection=connection) - return query_compiler.as_subquery_condition(self.alias, self.columns) + return query_compiler.as_subquery_condition(self.alias, self.columns, qn) - def relabeled_clone(self, relabels): + def relabel_aliases(self, change_map): + self.alias = change_map.get(self.alias, self.alias) + + def clone(self): return self.__class__( - relabels.get(self.alias, self.alias), - self.columns, self.query_object) + self.alias, self.columns, self.targets, + self.query_object) diff --git a/django/db/transaction.py b/django/db/transaction.py index 48e7f900dd..f770f2efa7 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -333,6 +333,23 @@ def atomic(using=None, savepoint=True): return Atomic(using, savepoint) +def _non_atomic_requests(view, using): + try: + view._non_atomic_requests.add(using) + except AttributeError: + view._non_atomic_requests = set([using]) + return view + + +def non_atomic_requests(using=None): + if callable(using): + return _non_atomic_requests(using, DEFAULT_DB_ALIAS) + else: + if using is None: + using = DEFAULT_DB_ALIAS + return lambda view: _non_atomic_requests(view, using) + + ############################################ # Deprecated decorators / context managers # ############################################ diff --git a/django/db/utils.py b/django/db/utils.py index e84060f9b3..bd7e10d24c 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -6,6 +6,7 @@ import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.utils.functional import cached_property from django.utils.importlib import import_module from django.utils.module_loading import import_by_path from django.utils._os import upath @@ -90,8 +91,7 @@ class DatabaseErrorWrapper(object): except AttributeError: args = (exc_value,) dj_exc_value = dj_exc_type(*args) - if six.PY3: - dj_exc_value.__cause__ = exc_value + dj_exc_value.__cause__ = exc_value # Only set the 'errors_occurred' flag for errors that may make # the connection unusable. if dj_exc_type not in (DataError, IntegrityError): @@ -138,16 +138,27 @@ class ConnectionDoesNotExist(Exception): class ConnectionHandler(object): - def __init__(self, databases): - if not databases: - self.databases = { + def __init__(self, databases=None): + """ + databases is an optional dictionary of database definitions (structured + like settings.DATABASES). + """ + self._databases = databases + self._connections = local() + + @cached_property + def databases(self): + if self._databases is None: + self._databases = settings.DATABASES + if self._databases == {}: + self._databases = { DEFAULT_DB_ALIAS: { 'ENGINE': 'django.db.backends.dummy', }, } - else: - self.databases = databases - self._connections = local() + if DEFAULT_DB_ALIAS not in self._databases: + raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS) + return self._databases def ensure_defaults(self, alias): """ @@ -202,14 +213,24 @@ class ConnectionHandler(object): class ConnectionRouter(object): - def __init__(self, routers): - self.routers = [] - for r in routers: + def __init__(self, routers=None): + """ + If routers is not specified, will default to settings.DATABASE_ROUTERS. + """ + self._routers = routers + + @cached_property + def routers(self): + if self._routers is None: + self._routers = settings.DATABASE_ROUTERS + routers = [] + for r in self._routers: if isinstance(r, six.string_types): router = import_by_path(r)() else: router = r - self.routers.append(router) + routers.append(router) + return routers def _router_func(action): def _route_db(self, model, **hints): diff --git a/django/forms/fields.py b/django/forms/fields.py index 4ce57d34a3..ac68b9f1fc 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -198,14 +198,15 @@ class Field(object): result.validators = self.validators[:] return result + class CharField(Field): def __init__(self, max_length=None, min_length=None, *args, **kwargs): self.max_length, self.min_length = max_length, min_length super(CharField, self).__init__(*args, **kwargs) if min_length is not None: - self.validators.append(validators.MinLengthValidator(min_length)) + self.validators.append(validators.MinLengthValidator(int(min_length))) if max_length is not None: - self.validators.append(validators.MaxLengthValidator(max_length)) + self.validators.append(validators.MaxLengthValidator(int(max_length))) def to_python(self, value): "Returns a Unicode object." @@ -220,6 +221,7 @@ class CharField(Field): attrs.update({'maxlength': str(self.max_length)}) return attrs + class IntegerField(Field): default_error_messages = { 'invalid': _('Enter a whole number.'), @@ -444,6 +446,7 @@ class TimeField(BaseTemporalField): def strptime(self, value, format): return datetime.datetime.strptime(force_str(value), format).time() + class DateTimeField(BaseTemporalField): widget = DateTimeInput input_formats = formats.get_format_lazy('DATETIME_INPUT_FORMATS') @@ -482,6 +485,7 @@ class DateTimeField(BaseTemporalField): def strptime(self, value, format): return datetime.datetime.strptime(force_str(value), format) + class RegexField(CharField): def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): """ @@ -511,6 +515,7 @@ class RegexField(CharField): regex = property(_get_regex, _set_regex) + class EmailField(CharField): widget = EmailInput default_validators = [validators.validate_email] @@ -519,6 +524,7 @@ class EmailField(CharField): value = self.to_python(value).strip() return super(EmailField, self).clean(value) + class FileField(Field): widget = ClearableFileInput default_error_messages = { @@ -626,6 +632,7 @@ class ImageField(FileField): f.seek(0) return f + class URLField(CharField): widget = URLInput default_error_messages = { @@ -670,6 +677,10 @@ class URLField(CharField): value = urlunsplit(url_fields) return value + def clean(self, value): + value = self.to_python(value).strip() + return super(URLField, self).clean(value) + class BooleanField(Field): widget = CheckboxInput @@ -788,6 +799,7 @@ class ChoiceField(Field): return True return False + class TypedChoiceField(ChoiceField): def __init__(self, *args, **kwargs): self.coerce = kwargs.pop('coerce', lambda val: val) @@ -899,6 +911,7 @@ class ComboField(Field): value = field.clean(value) return value + class MultiValueField(Field): """ A Field that aggregates the logic of multiple Fields. @@ -1043,6 +1056,7 @@ class FilePathField(ChoiceField): self.widget.choices = self.choices + class SplitDateTimeField(MultiValueField): widget = SplitDateTimeWidget hidden_widget = SplitHiddenDateTimeWidget @@ -1105,3 +1119,7 @@ class GenericIPAddressField(CharField): class SlugField(CharField): default_validators = [validators.validate_slug] + + def clean(self, value): + value = self.to_python(value).strip() + return super(SlugField, self).clean(value) diff --git a/django/forms/forms.py b/django/forms/forms.py index b231de421a..0c598ac775 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -134,7 +134,7 @@ class BaseForm(object): Subclasses may wish to override. """ - return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name + return '%s-%s' % (self.prefix, field_name) if self.prefix else field_name def add_initial_prefix(self, field_name): """ @@ -342,6 +342,8 @@ class BaseForm(object): data_value = field.widget.value_from_datadict(self.data, self.files, prefixed_name) if not field.show_hidden_initial: initial_value = self.initial.get(name, field.initial) + if callable(initial_value): + initial_value = initial_value() else: initial_prefixed_name = self.add_initial_prefix(name) hidden_widget = field.hidden_widget() @@ -523,10 +525,11 @@ class BoundField(object): widget = self.field.widget id_ = widget.attrs.get('id') or self.auto_id if id_: + id_for_label = widget.id_for_label(id_) + if id_for_label: + attrs = dict(attrs or {}, **{'for': id_for_label}) attrs = flatatt(attrs) if attrs else '' - contents = format_html('<label for="{0}"{1}>{2}</label>', - widget.id_for_label(id_), attrs, contents - ) + contents = format_html('<label{0}>{1}</label>', attrs, contents) else: contents = conditional_escape(contents) return mark_safe(contents) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index d421770093..fd98c43405 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -250,9 +250,9 @@ class BaseFormSet(object): form -- i.e., from formset.clean(). Returns an empty ErrorList if there are none. """ - if self._non_form_errors is not None: - return self._non_form_errors - return self.error_class() + if self._non_form_errors is None: + self.full_clean() + return self._non_form_errors @property def errors(self): @@ -291,16 +291,20 @@ class BaseFormSet(object): def full_clean(self): """ - Cleans all of self.data and populates self._errors. + Cleans all of self.data and populates self._errors and + self._non_form_errors. """ self._errors = [] + self._non_form_errors = self.error_class() + if not self.is_bound: # Stop further processing. return for i in range(0, self.total_form_count()): form = self.forms[i] self._errors.append(form.errors) try: - if (self.validate_max and self.total_form_count() > self.max_num) or \ + if (self.validate_max and + self.total_form_count() - len(self.deleted_forms) > self.max_num) or \ self.management_form.cleaned_data[TOTAL_FORM_COUNT] > self.absolute_max: raise ValidationError(ungettext( "Please submit %d or fewer forms.", diff --git a/django/forms/models.py b/django/forms/models.py index af5cda8faf..65434a6f6e 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -13,12 +13,12 @@ from django.forms.forms import BaseForm, get_declared_fields from django.forms.formsets import BaseFormSet, formset_factory from django.forms.util import ErrorList from django.forms.widgets import (SelectMultiple, HiddenInput, - MultipleHiddenInput, media_property) + MultipleHiddenInput, media_property, CheckboxSelectMultiple) from django.utils.encoding import smart_text, force_text from django.utils.datastructures import SortedDict from django.utils import six from django.utils.text import get_text_list, capfirst -from django.utils.translation import ugettext_lazy as _, ugettext +from django.utils.translation import ugettext_lazy as _, ugettext, string_concat __all__ = ( @@ -85,6 +85,8 @@ def save_instance(form, instance, fields=None, fail_message='saved', for f in opts.many_to_many: if fields and f.name not in fields: continue + if exclude and f.name in exclude: + continue if f.name in cleaned_data: f.save_form_data(instance, cleaned_data[f.name]) if commit: @@ -136,7 +138,7 @@ def model_to_dict(instance, fields=None, exclude=None): data[f.name] = f.value_from_object(instance) return data -def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None): +def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_callback=None, localized_fields=None): """ Returns a ``SortedDict`` containing form fields for the given model. @@ -162,10 +164,12 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None, formfield_c continue if exclude and f.name in exclude: continue + + kwargs = {} if widgets and f.name in widgets: - kwargs = {'widget': widgets[f.name]} - else: - kwargs = {} + kwargs['widget'] = widgets[f.name] + if localized_fields == ALL_FIELDS or (localized_fields and f.name in localized_fields): + kwargs['localize'] = True if formfield_callback is None: formfield = f.formfield(**kwargs) @@ -192,6 +196,7 @@ class ModelFormOptions(object): self.fields = getattr(options, 'fields', None) self.exclude = getattr(options, 'exclude', None) self.widgets = getattr(options, 'widgets', None) + self.localized_fields = getattr(options, 'localized_fields', None) class ModelFormMetaclass(type): @@ -215,7 +220,7 @@ class ModelFormMetaclass(type): # We check if a string was passed to `fields` or `exclude`, # which is likely to be a mistake where the user typed ('foo') instead # of ('foo',) - for opt in ['fields', 'exclude']: + for opt in ['fields', 'exclude', 'localized_fields']: value = getattr(opts, opt) if isinstance(value, six.string_types) and value != ALL_FIELDS: msg = ("%(model)s.Meta.%(opt)s cannot be a string. " @@ -235,15 +240,16 @@ class ModelFormMetaclass(type): warnings.warn("Creating a ModelForm without either the 'fields' attribute " "or the 'exclude' attribute is deprecated - form %s " "needs updating" % name, - PendingDeprecationWarning) + PendingDeprecationWarning, stacklevel=2) if opts.fields == ALL_FIELDS: # sentinel for fields_for_model to indicate "get the list of # fields from the model" opts.fields = None - fields = fields_for_model(opts.model, opts.fields, - opts.exclude, opts.widgets, formfield_callback) + fields = fields_for_model(opts.model, opts.fields, opts.exclude, + opts.widgets, formfield_callback, opts.localized_fields) + # make sure opts.fields doesn't specify an invalid field none_model_fields = [k for k, v in six.iteritems(fields) if not v] missing_fields = set(none_model_fields) - \ @@ -401,7 +407,8 @@ class BaseModelForm(BaseForm): else: fail_message = 'changed' return save_instance(self, self.instance, self._meta.fields, - fail_message, commit, construct=False) + fail_message, commit, self._meta.exclude, + construct=False) save.alters_data = True @@ -409,7 +416,7 @@ class ModelForm(six.with_metaclass(ModelFormMetaclass, BaseModelForm)): pass def modelform_factory(model, form=ModelForm, fields=None, exclude=None, - formfield_callback=None, widgets=None): + formfield_callback=None, widgets=None, localized_fields=None): """ Returns a ModelForm containing form fields for the given model. @@ -423,6 +430,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, ``widgets`` is a dictionary of model field names mapped to a widget. + ``localized_fields`` is a list of names of fields which should be localized. + ``formfield_callback`` is a callable that takes a model field and returns a form field. """ @@ -438,6 +447,8 @@ def modelform_factory(model, form=ModelForm, fields=None, exclude=None, attrs['exclude'] = exclude if widgets is not None: attrs['widgets'] = widgets + if localized_fields is not None: + attrs['localized_fields'] = localized_fields # If parent form class already has an inner Meta, the Meta we're # creating needs to inherit from the parent's inner meta. @@ -726,8 +737,8 @@ class BaseModelFormSet(BaseFormSet): def modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, - can_order=False, max_num=None, fields=None, - exclude=None, widgets=None, validate_max=False): + can_order=False, max_num=None, fields=None, exclude=None, + widgets=None, validate_max=False, localized_fields=None): """ Returns a FormSet class for the given Django model class. """ @@ -748,7 +759,7 @@ def modelformset_factory(model, form=ModelForm, formfield_callback=None, form = modelform_factory(model, form=form, fields=fields, exclude=exclude, formfield_callback=formfield_callback, - widgets=widgets) + widgets=widgets, localized_fields=localized_fields) FormSet = formset_factory(form, formset, extra=extra, max_num=max_num, can_order=can_order, can_delete=can_delete, validate_max=validate_max) @@ -885,9 +896,9 @@ def _get_foreign_key(parent_model, model, fk_name=None, can_fail=False): def inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, - fields=None, exclude=None, - extra=3, can_order=False, can_delete=True, max_num=None, - formfield_callback=None, widgets=None, validate_max=False): + fields=None, exclude=None, extra=3, can_order=False, + can_delete=True, max_num=None, formfield_callback=None, + widgets=None, validate_max=False, localized_fields=None): """ Returns an ``InlineFormSet`` for the given kwargs. @@ -910,6 +921,7 @@ def inlineformset_factory(parent_model, model, form=ModelForm, 'max_num': max_num, 'widgets': widgets, 'validate_max': validate_max, + 'localized_fields': localized_fields, } FormSet = modelformset_factory(model, **kwargs) FormSet.fk = fk @@ -1095,6 +1107,10 @@ class ModelMultipleChoiceField(ModelChoiceField): super(ModelMultipleChoiceField, self).__init__(queryset, None, cache_choices, required, widget, label, initial, help_text, *args, **kwargs) + # Remove this in Django 1.8 + if isinstance(self.widget, SelectMultiple) and not isinstance(self.widget, CheckboxSelectMultiple): + msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.') + self.help_text = string_concat(self.help_text, ' ', msg) def clean(self, value): if self.required and not value: diff --git a/django/http/multipartparser.py b/django/http/multipartparser.py index 0e999f2ded..eeb435fa57 100644 --- a/django/http/multipartparser.py +++ b/django/http/multipartparser.py @@ -11,7 +11,7 @@ import cgi import sys from django.conf import settings -from django.core.exceptions import SuspiciousOperation +from django.core.exceptions import SuspiciousMultipartForm from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_text from django.utils import six @@ -48,9 +48,9 @@ class MultiPartParser(object): The standard ``META`` dictionary in Django request objects. :input_data: The raw post data, as a file-like object. - :upload_handler: - An UploadHandler instance that performs operations on the uploaded - data. + :upload_handlers: + A list of UploadHandler instances that perform operations on the uploaded + data. :encoding: The encoding with which to treat the incoming data. """ @@ -113,14 +113,15 @@ class MultiPartParser(object): if self._content_length == 0: return QueryDict('', encoding=self._encoding), MultiValueDict() - # See if the handler will want to take care of the parsing. - # This allows overriding everything if somebody wants it. + # See if any of the handlers take care of the parsing. + # This allows overriding everything if need be. for handler in handlers: result = handler.handle_raw_input(self._input_data, self._meta, self._content_length, self._boundary, encoding) + #Check to see if it was handled if result is not None: return result[0], result[1] @@ -369,7 +370,7 @@ class LazyStream(six.Iterator): if current_number == num_bytes]) if number_equal > 40: - raise SuspiciousOperation( + raise SuspiciousMultipartForm( "The multipart parser got stuck, which shouldn't happen with" " normal uploaded files. Check for malicious upload activity;" " if there is none, report this to the Django developers." diff --git a/django/http/request.py b/django/http/request.py index 749b9f2561..37aa1a355a 100644 --- a/django/http/request.py +++ b/django/http/request.py @@ -14,7 +14,7 @@ except ImportError: from django.conf import settings from django.core import signing -from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured +from django.core.exceptions import DisallowedHost, ImproperlyConfigured from django.core.files import uploadhandler from django.http.multipartparser import MultiPartParser from django.utils import six @@ -72,7 +72,7 @@ class HttpRequest(object): msg = "Invalid HTTP_HOST header: %r." % host if domain: msg += "You may need to add %r to ALLOWED_HOSTS." % domain - raise SuspiciousOperation(msg) + raise DisallowedHost(msg) def get_full_path(self): # RFC 3986 requires query string arguments to be in the ASCII range. @@ -238,11 +238,17 @@ class HttpRequest(object): def read(self, *args, **kwargs): self._read_started = True - return self._stream.read(*args, **kwargs) + try: + return self._stream.read(*args, **kwargs) + except IOError as e: + six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2]) def readline(self, *args, **kwargs): self._read_started = True - return self._stream.readline(*args, **kwargs) + try: + return self._stream.readline(*args, **kwargs) + except IOError as e: + six.reraise(UnreadablePostError, UnreadablePostError(*e.args), sys.exc_info()[2]) def xreadlines(self): while True: diff --git a/django/http/response.py b/django/http/response.py index 88ac8848c2..9aa49b1d5f 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -12,7 +12,7 @@ except ImportError: from django.conf import settings from django.core import signals from django.core import signing -from django.core.exceptions import SuspiciousOperation +from django.core.exceptions import DisallowedRedirect from django.http.cookie import SimpleCookie from django.utils import six, timezone from django.utils.encoding import force_bytes, iri_to_uri @@ -20,6 +20,65 @@ from django.utils.http import cookie_date from django.utils.six.moves import map +# See http://www.iana.org/assignments/http-status-codes +REASON_PHRASES = { + 100: 'CONTINUE', + 101: 'SWITCHING PROTOCOLS', + 102: 'PROCESSING', + 200: 'OK', + 201: 'CREATED', + 202: 'ACCEPTED', + 203: 'NON-AUTHORITATIVE INFORMATION', + 204: 'NO CONTENT', + 205: 'RESET CONTENT', + 206: 'PARTIAL CONTENT', + 207: 'MULTI-STATUS', + 208: 'ALREADY REPORTED', + 226: 'IM USED', + 300: 'MULTIPLE CHOICES', + 301: 'MOVED PERMANENTLY', + 302: 'FOUND', + 303: 'SEE OTHER', + 304: 'NOT MODIFIED', + 305: 'USE PROXY', + 306: 'RESERVED', + 307: 'TEMPORARY REDIRECT', + 400: 'BAD REQUEST', + 401: 'UNAUTHORIZED', + 402: 'PAYMENT REQUIRED', + 403: 'FORBIDDEN', + 404: 'NOT FOUND', + 405: 'METHOD NOT ALLOWED', + 406: 'NOT ACCEPTABLE', + 407: 'PROXY AUTHENTICATION REQUIRED', + 408: 'REQUEST TIMEOUT', + 409: 'CONFLICT', + 410: 'GONE', + 411: 'LENGTH REQUIRED', + 412: 'PRECONDITION FAILED', + 413: 'REQUEST ENTITY TOO LARGE', + 414: 'REQUEST-URI TOO LONG', + 415: 'UNSUPPORTED MEDIA TYPE', + 416: 'REQUESTED RANGE NOT SATISFIABLE', + 417: 'EXPECTATION FAILED', + 418: "I'M A TEAPOT", + 422: 'UNPROCESSABLE ENTITY', + 423: 'LOCKED', + 424: 'FAILED DEPENDENCY', + 426: 'UPGRADE REQUIRED', + 500: 'INTERNAL SERVER ERROR', + 501: 'NOT IMPLEMENTED', + 502: 'BAD GATEWAY', + 503: 'SERVICE UNAVAILABLE', + 504: 'GATEWAY TIMEOUT', + 505: 'HTTP VERSION NOT SUPPORTED', + 506: 'VARIANT ALSO NEGOTIATES', + 507: 'INSUFFICIENT STORAGE', + 508: 'LOOP DETECTED', + 510: 'NOT EXTENDED', +} + + class BadHeaderError(ValueError): pass @@ -33,8 +92,9 @@ class HttpResponseBase(six.Iterator): """ status_code = 200 + reason_phrase = None # Use default reason phrase for status code. - def __init__(self, content_type=None, status=None, mimetype=None): + def __init__(self, content_type=None, status=None, reason=None, mimetype=None): # _headers is a mapping of the lower-case name to the original case of # the header (required for working with legacy systems) and the header # value. Both the name of the header and its value are ASCII strings. @@ -53,9 +113,13 @@ class HttpResponseBase(six.Iterator): content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, self._charset) self.cookies = SimpleCookie() - if status: + if status is not None: self.status_code = status - + if reason is not None: + self.reason_phrase = reason + elif self.reason_phrase is None: + self.reason_phrase = REASON_PHRASES.get(self.status_code, + 'UNKNOWN STATUS CODE') self['Content-Type'] = content_type def serialize_headers(self): @@ -388,7 +452,7 @@ class HttpResponseRedirectBase(HttpResponse): def __init__(self, redirect_to, *args, **kwargs): parsed = urlparse(redirect_to) if parsed.scheme and parsed.scheme not in self.allowed_schemes: - raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) + raise DisallowedRedirect("Unsafe redirect to URL with protocol '%s'" % parsed.scheme) super(HttpResponseRedirectBase, self).__init__(*args, **kwargs) self['Location'] = iri_to_uri(redirect_to) diff --git a/django/http/utils.py b/django/http/utils.py index fcb3fecb6c..e13dc4cbb6 100644 --- a/django/http/utils.py +++ b/django/http/utils.py @@ -31,13 +31,13 @@ def conditional_content_removal(request, response): if response.streaming: response.streaming_content = [] else: - response.content = '' + response.content = b'' response['Content-Length'] = '0' if request.method == 'HEAD': if response.streaming: response.streaming_content = [] else: - response.content = '' + response.content = b'' return response diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 83860e15f3..e13a8c3918 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -29,11 +29,6 @@ More details about how the caching works: of the response's "Cache-Control" header, falling back to the CACHE_MIDDLEWARE_SECONDS setting if the section was not found. -* If CACHE_MIDDLEWARE_ANONYMOUS_ONLY is set to True, only anonymous requests - (i.e., those not made by a logged-in user) will be cached. This is a simple - and effective way of avoiding the caching of the Django admin (and any other - user-specific content). - * This middleware expects that a HEAD request is answered with the same response headers exactly like the corresponding GET request. @@ -48,6 +43,8 @@ More details about how the caching works: """ +import warnings + from django.conf import settings from django.core.cache import get_cache, DEFAULT_CACHE_ALIAS from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age @@ -200,5 +197,9 @@ class CacheMiddleware(UpdateCacheMiddleware, FetchFromCacheMiddleware): else: self.cache_anonymous_only = cache_anonymous_only + if self.cache_anonymous_only: + msg = "CACHE_MIDDLEWARE_ANONYMOUS_ONLY has been deprecated and will be removed in Django 1.8." + warnings.warn(msg, PendingDeprecationWarning, stacklevel=1) + self.cache = get_cache(self.cache_alias, **cache_kwargs) self.cache_timeout = self.cache.default_timeout diff --git a/django/middleware/common.py b/django/middleware/common.py index 92f8cb3992..2c76c47756 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -7,6 +7,7 @@ from django.conf import settings from django.core.mail import mail_managers from django.core import urlresolvers from django import http +from django.utils.encoding import force_text from django.utils.http import urlquote from django.utils import six @@ -84,7 +85,7 @@ class CommonMiddleware(object): return if new_url[0]: newurl = "%s://%s%s" % ( - request.is_secure() and 'https' or 'http', + 'https' if request.is_secure() else 'http', new_url[0], urlquote(new_url[1])) else: newurl = urlquote(new_url[1]) @@ -140,16 +141,18 @@ class BrokenLinkEmailsMiddleware(object): if response.status_code == 404 and not settings.DEBUG: domain = request.get_host() path = request.get_full_path() - referer = request.META.get('HTTP_REFERER', '') - is_internal = self.is_internal_request(domain, referer) - is_not_search_engine = '?' not in referer - is_ignorable = self.is_ignorable_404(path) - if referer and (is_internal or is_not_search_engine) and not is_ignorable: + referer = force_text(request.META.get('HTTP_REFERER', ''), errors='replace') + + if not self.is_ignorable_request(request, path, domain, referer): ua = request.META.get('HTTP_USER_AGENT', '<none>') ip = request.META.get('REMOTE_ADDR', '<none>') mail_managers( - "Broken %slink on %s" % (('INTERNAL ' if is_internal else ''), domain), - "Referrer: %s\nRequested URL: %s\nUser agent: %s\nIP address: %s\n" % (referer, path, ua, ip), + "Broken %slink on %s" % ( + ('INTERNAL ' if self.is_internal_request(domain, referer) else ''), + domain + ), + "Referrer: %s\nRequested URL: %s\nUser agent: %s\n" + "IP address: %s\n" % (referer, path, ua, ip), fail_silently=True) return response @@ -158,10 +161,14 @@ class BrokenLinkEmailsMiddleware(object): Returns True if the referring URL is the same domain as the current request. """ # Different subdomains are treated as different domains. - return re.match("^https?://%s/" % re.escape(domain), referer) + return bool(re.match("^https?://%s/" % re.escape(domain), referer)) - def is_ignorable_404(self, uri): + def is_ignorable_request(self, request, uri, domain, referer): """ - Returns True if a 404 at the given URL *shouldn't* notify the site managers. + Returns True if the given request *shouldn't* notify the site managers. """ + # '?' in referer is identified as search engine source + if (not referer or + (not self.is_internal_request(domain, referer) and '?' in referer)): + return True return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS) diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 423034478b..1b5732fbbf 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -53,6 +53,14 @@ def get_token(request): return request.META.get("CSRF_COOKIE", None) +def rotate_token(request): + """ + Changes the CSRF token in use for a request - should be done on login + for security purposes. + """ + request.META["CSRF_COOKIE"] = _get_new_csrf_key() + + def _sanitize_token(token): # Allow only alphanum if len(token) > CSRF_KEY_LENGTH: @@ -83,6 +91,13 @@ class CsrfViewMiddleware(object): return None def _reject(self, request, reason): + logger.warning('Forbidden (%s): %s', + reason, request.path, + extra={ + 'status_code': 403, + 'request': request, + } + ) return _get_failure_view()(request, reason=reason) def process_view(self, request, callback, callback_args, callback_kwargs): @@ -134,38 +149,18 @@ class CsrfViewMiddleware(object): # we can use strict Referer checking. referer = request.META.get('HTTP_REFERER') if referer is None: - logger.warning('Forbidden (%s): %s', - REASON_NO_REFERER, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_NO_REFERER) # Note that request.get_host() includes the port. good_referer = 'https://%s/' % request.get_host() if not same_origin(referer, good_referer): reason = REASON_BAD_REFERER % (referer, good_referer) - logger.warning('Forbidden (%s): %s', reason, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, reason) if csrf_token is None: # No CSRF cookie. For POST requests, we insist on a CSRF cookie, # and in this way we can avoid all CSRF attacks, including login # CSRF. - logger.warning('Forbidden (%s): %s', - REASON_NO_CSRF_COOKIE, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_NO_CSRF_COOKIE) # Check non-cookie token for match. @@ -179,13 +174,6 @@ class CsrfViewMiddleware(object): request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') if not constant_time_compare(request_csrf_token, csrf_token): - logger.warning('Forbidden (%s): %s', - REASON_BAD_TOKEN, request.path, - extra={ - 'status_code': 403, - 'request': request, - } - ) return self._reject(request, REASON_BAD_TOKEN) return self._accept(request) diff --git a/django/middleware/doc.py b/django/middleware/doc.py index ee3fe2cb2f..1af7b6150a 100644 --- a/django/middleware/doc.py +++ b/django/middleware/doc.py @@ -1,23 +1,6 @@ -from django.conf import settings -from django import http +"""XViewMiddleware has been moved to django.contrib.admindocs.middleware.""" -class XViewMiddleware(object): - """ - Adds an X-View header to internal HEAD requests -- used by the documentation system. - """ - def process_view(self, request, view_func, view_args, view_kwargs): - """ - If the request method is HEAD and either the IP is internal or the - user is a logged-in staff member, quickly return with an x-header - indicating the view function. This is used by the documentation module - to lookup the view function for an arbitrary page. - """ - assert hasattr(request, 'user'), ( - "The XView middleware requires authentication middleware to be " - "installed. Edit your MIDDLEWARE_CLASSES setting to insert " - "'django.contrib.auth.middleware.AuthenticationMiddleware'.") - if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or - (request.user.is_active and request.user.is_staff)): - response = http.HttpResponse() - response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) - return response +import warnings +warnings.warn(__doc__, PendingDeprecationWarning, stacklevel=2) + +from django.contrib.admindocs.middleware import XViewMiddleware diff --git a/django/middleware/locale.py b/django/middleware/locale.py index 9b2ef8ff32..4e0a4753ce 100644 --- a/django/middleware/locale.py +++ b/django/middleware/locale.py @@ -6,6 +6,7 @@ from django.core.urlresolvers import (is_valid_path, get_resolver, from django.http import HttpResponseRedirect from django.utils.cache import patch_vary_headers from django.utils import translation +from django.utils.datastructures import SortedDict class LocaleMiddleware(object): @@ -18,7 +19,7 @@ class LocaleMiddleware(object): """ def __init__(self): - self._supported_languages = dict(settings.LANGUAGES) + self._supported_languages = SortedDict(settings.LANGUAGES) self._is_language_prefix_patterns_used = False for url_pattern in get_resolver(None).url_patterns: if isinstance(url_pattern, LocaleRegexURLResolver): @@ -48,10 +49,14 @@ class LocaleMiddleware(object): if path_valid: language_url = "%s://%s/%s%s" % ( - request.is_secure() and 'https' or 'http', + 'https' if request.is_secure() else 'http', request.get_host(), language, request.get_full_path()) return HttpResponseRedirect(language_url) + # Store language back into session if it is not present + if hasattr(request, 'session'): + request.session.setdefault('django_language', language) + if not (self.is_language_prefix_patterns_used() and language_from_path): patch_vary_headers(response, ('Accept-Language',)) diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 88526e5a20..4201cfeb67 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -14,7 +14,7 @@ from django.utils import formats from django.utils.dateformat import format, time_format from django.utils.encoding import force_text, iri_to_uri from django.utils.html import (conditional_escape, escapejs, fix_ampersands, - escape, urlize as urlize_impl, linebreaks, strip_tags) + escape, urlize as urlize_impl, linebreaks, strip_tags, avoid_wrapping) from django.utils.http import urlquote from django.utils.text import Truncator, wrap, phone2numeric from django.utils.safestring import mark_safe, SafeData, mark_for_escaping @@ -810,7 +810,8 @@ def filesizeformat(bytes): try: bytes = float(bytes) except (TypeError,ValueError,UnicodeDecodeError): - return ungettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0} + value = ungettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0} + return avoid_wrapping(value) filesize_number_format = lambda value: formats.number_format(round(value, 1), 1) @@ -821,16 +822,19 @@ def filesizeformat(bytes): PB = 1<<50 if bytes < KB: - return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} - if bytes < MB: - return ugettext("%s KB") % filesize_number_format(bytes / KB) - if bytes < GB: - return ugettext("%s MB") % filesize_number_format(bytes / MB) - if bytes < TB: - return ugettext("%s GB") % filesize_number_format(bytes / GB) - if bytes < PB: - return ugettext("%s TB") % filesize_number_format(bytes / TB) - return ugettext("%s PB") % filesize_number_format(bytes / PB) + value = ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} + elif bytes < MB: + value = ugettext("%s KB") % filesize_number_format(bytes / KB) + elif bytes < GB: + value = ugettext("%s MB") % filesize_number_format(bytes / MB) + elif bytes < TB: + value = ugettext("%s GB") % filesize_number_format(bytes / GB) + elif bytes < PB: + value = ugettext("%s TB") % filesize_number_format(bytes / TB) + else: + value = ugettext("%s PB") % filesize_number_format(bytes / PB) + + return avoid_wrapping(value) @register.filter(is_safe=False) def pluralize(value, arg='s'): diff --git a/django/test/client.py b/django/test/client.py index 46f55d7cdc..2ed0df8fea 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -13,7 +13,7 @@ except ImportError: # Python 2 from urlparse import urlparse, urlsplit from django.conf import settings -from django.contrib.auth import authenticate, login +from django.contrib.auth import authenticate, login, logout, get_user_model from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest from django.core.signals import (request_started, request_finished, @@ -571,11 +571,17 @@ class Client(RequestFactory): Causes the authenticated user to be logged out. """ - session = import_module(settings.SESSION_ENGINE).SessionStore() - session_cookie = self.cookies.get(settings.SESSION_COOKIE_NAME) - if session_cookie: - session.delete(session_key=session_cookie.value) - self.cookies = SimpleCookie() + request = HttpRequest() + engine = import_module(settings.SESSION_ENGINE) + UserModel = get_user_model() + if self.session: + request.session = self.session + uid = self.session.get("_auth_user_id") + if uid: + request.user = UserModel._default_manager.get(pk=uid) + else: + request.session = engine.SessionStore() + logout(request) def _handle_redirects(self, response, **extra): "Follows any redirects by requesting responses from the server using GET." diff --git a/django/test/simple.py b/django/test/simple.py index 5117c6452f..f28b8a2830 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -3,14 +3,15 @@ This module is pending deprecation as of Django 1.6 and will be removed in version 1.8. """ - +import json +import re import unittest as real_unittest import warnings from django.db.models import get_app, get_apps from django.test import _doctest as doctest from django.test import runner -from django.test.testcases import OutputChecker, DocTestRunner +from django.test.utils import compare_xml, strip_quotes from django.utils import unittest from django.utils.importlib import import_module from django.utils.module_loading import module_has_submodule @@ -25,6 +26,71 @@ warnings.warn( # The module name for tests outside models.py TEST_MODULE = 'tests' + +normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s) +normalize_decimals = lambda s: re.sub(r"Decimal\('(\d+(\.\d*)?)'\)", + lambda m: "Decimal(\"%s\")" % m.groups()[0], s) + + +class OutputChecker(doctest.OutputChecker): + def check_output(self, want, got, optionflags): + """ + The entry method for doctest output checking. Defers to a sequence of + child checkers + """ + checks = (self.check_output_default, + self.check_output_numeric, + self.check_output_xml, + self.check_output_json) + for check in checks: + if check(want, got, optionflags): + return True + return False + + def check_output_default(self, want, got, optionflags): + """ + The default comparator provided by doctest - not perfect, but good for + most purposes + """ + return doctest.OutputChecker.check_output(self, want, got, optionflags) + + def check_output_numeric(self, want, got, optionflags): + """Doctest does an exact string comparison of output, which means that + some numerically equivalent values aren't equal. This check normalizes + * long integers (22L) so that they equal normal integers. (22) + * Decimals so that they are comparable, regardless of the change + made to __repr__ in Python 2.6. + """ + return doctest.OutputChecker.check_output(self, + normalize_decimals(normalize_long_ints(want)), + normalize_decimals(normalize_long_ints(got)), + optionflags) + + def check_output_xml(self, want, got, optionsflags): + try: + return compare_xml(want, got) + except Exception: + return False + + def check_output_json(self, want, got, optionsflags): + """ + Tries to compare want and got as if they were JSON-encoded data + """ + want, got = strip_quotes(want, got) + try: + want_json = json.loads(want) + got_json = json.loads(got) + except Exception: + return False + return want_json == got_json + + +class DocTestRunner(doctest.DocTestRunner): + def __init__(self, *args, **kwargs): + doctest.DocTestRunner.__init__(self, *args, **kwargs) + self.optionflags = doctest.ELLIPSIS + + doctestOutputChecker = OutputChecker() diff --git a/django/test/testcases.py b/django/test/testcases.py index 6fe6b9c397..08c03154b1 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -30,12 +30,11 @@ from django.core.urlresolvers import clear_url_caches, set_urlconf from django.db import connection, connections, DEFAULT_DB_ALIAS, transaction from django.forms.fields import CharField from django.http import QueryDict -from django.test import _doctest as doctest from django.test.client import Client from django.test.html import HTMLParseError, parse_html from django.test.signals import template_rendered from django.test.utils import (CaptureQueriesContext, ContextList, - override_settings, compare_xml, strip_quotes) + override_settings, compare_xml) from django.utils import six, unittest as ut2 from django.utils.encoding import force_text from django.utils.unittest import skipIf # Imported here for backward compatibility @@ -43,15 +42,10 @@ from django.utils.unittest.util import safe_repr from django.views.static import serve -__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase', +__all__ = ('TestCase', 'TransactionTestCase', 'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature') -normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s) -normalize_decimals = lambda s: re.sub(r"Decimal\('(\d+(\.\d*)?)'\)", - lambda m: "Decimal(\"%s\")" % m.groups()[0], s) - - def to_list(value): """ Puts value into a list if it's not already one. @@ -96,75 +90,6 @@ def assert_and_parse_html(self, html, user_msg, msg): return dom -class OutputChecker(doctest.OutputChecker): - def __init__(self): - warnings.warn( - "The django.test.testcases.OutputChecker class is deprecated; " - "use the doctest module from the Python standard library instead.", - PendingDeprecationWarning) - - def check_output(self, want, got, optionflags): - """ - The entry method for doctest output checking. Defers to a sequence of - child checkers - """ - checks = (self.check_output_default, - self.check_output_numeric, - self.check_output_xml, - self.check_output_json) - for check in checks: - if check(want, got, optionflags): - return True - return False - - def check_output_default(self, want, got, optionflags): - """ - The default comparator provided by doctest - not perfect, but good for - most purposes - """ - return doctest.OutputChecker.check_output(self, want, got, optionflags) - - def check_output_numeric(self, want, got, optionflags): - """Doctest does an exact string comparison of output, which means that - some numerically equivalent values aren't equal. This check normalizes - * long integers (22L) so that they equal normal integers. (22) - * Decimals so that they are comparable, regardless of the change - made to __repr__ in Python 2.6. - """ - return doctest.OutputChecker.check_output(self, - normalize_decimals(normalize_long_ints(want)), - normalize_decimals(normalize_long_ints(got)), - optionflags) - - def check_output_xml(self, want, got, optionsflags): - try: - return compare_xml(want, got) - except Exception: - return False - - def check_output_json(self, want, got, optionsflags): - """ - Tries to compare want and got as if they were JSON-encoded data - """ - want, got = strip_quotes(want, got) - try: - want_json = json.loads(want) - got_json = json.loads(got) - except Exception: - return False - return want_json == got_json - - -class DocTestRunner(doctest.DocTestRunner): - def __init__(self, *args, **kwargs): - warnings.warn( - "The django.test.testcases.DocTestRunner class is deprecated; " - "use the doctest module from the Python standard library instead.", - PendingDeprecationWarning) - doctest.DocTestRunner.__init__(self, *args, **kwargs) - self.optionflags = doctest.ELLIPSIS - - class _AssertNumQueriesContext(CaptureQueriesContext): def __init__(self, test_case, num, connection): self.test_case = test_case @@ -231,6 +156,10 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): class SimpleTestCase(ut2.TestCase): + # The class we'll use for the test client self.client. + # Can be overridden in derived classes. + client_class = Client + _warn_txt = ("save_warnings_state/restore_warnings_state " "django.test.*TestCase methods are deprecated. Use Python's " "warnings.catch_warnings context manager instead.") @@ -264,10 +193,31 @@ class SimpleTestCase(ut2.TestCase): return def _pre_setup(self): - pass + """Performs any pre-test setup. This includes: + + * If the Test Case class has a 'urls' member, replace the + ROOT_URLCONF with it. + * Clearing the mail test outbox. + """ + self.client = self.client_class() + self._urlconf_setup() + mail.outbox = [] + + def _urlconf_setup(self): + set_urlconf(None) + if hasattr(self, 'urls'): + self._old_root_urlconf = settings.ROOT_URLCONF + settings.ROOT_URLCONF = self.urls + clear_url_caches() def _post_teardown(self): - pass + self._urlconf_teardown() + + def _urlconf_teardown(self): + set_urlconf(None) + if hasattr(self, '_old_root_urlconf'): + settings.ROOT_URLCONF = self._old_root_urlconf + clear_url_caches() def save_warnings_state(self): """ @@ -291,258 +241,6 @@ class SimpleTestCase(ut2.TestCase): """ return override_settings(**kwargs) - def assertRaisesMessage(self, expected_exception, expected_message, - callable_obj=None, *args, **kwargs): - """ - Asserts that the message in a raised exception matches the passed - value. - - Args: - expected_exception: Exception class expected to be raised. - expected_message: expected error message string value. - callable_obj: Function to be called. - args: Extra args. - kwargs: Extra kwargs. - """ - return six.assertRaisesRegex(self, expected_exception, - re.escape(expected_message), callable_obj, *args, **kwargs) - - def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, - field_kwargs=None, empty_value=''): - """ - Asserts that a form field behaves correctly with various inputs. - - Args: - fieldclass: the class of the field to be tested. - valid: a dictionary mapping valid inputs to their expected - cleaned values. - invalid: a dictionary mapping invalid inputs to one or more - raised error messages. - field_args: the args passed to instantiate the field - field_kwargs: the kwargs passed to instantiate the field - empty_value: the expected clean output for inputs in empty_values - - """ - if field_args is None: - field_args = [] - if field_kwargs is None: - field_kwargs = {} - required = fieldclass(*field_args, **field_kwargs) - optional = fieldclass(*field_args, - **dict(field_kwargs, required=False)) - # test valid inputs - for input, output in valid.items(): - self.assertEqual(required.clean(input), output) - self.assertEqual(optional.clean(input), output) - # test invalid inputs - for input, errors in invalid.items(): - with self.assertRaises(ValidationError) as context_manager: - required.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - - with self.assertRaises(ValidationError) as context_manager: - optional.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - # test required inputs - error_required = [force_text(required.error_messages['required'])] - for e in required.empty_values: - with self.assertRaises(ValidationError) as context_manager: - required.clean(e) - self.assertEqual(context_manager.exception.messages, - error_required) - self.assertEqual(optional.clean(e), empty_value) - # test that max_length and min_length are always accepted - if issubclass(fieldclass, CharField): - field_kwargs.update({'min_length':2, 'max_length':20}) - self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), - fieldclass)) - - def assertHTMLEqual(self, html1, html2, msg=None): - """ - Asserts that two HTML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid HTML. - """ - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 != dom2: - standardMsg = '%s != %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - diff = ('\n' + '\n'.join(difflib.ndiff( - six.text_type(dom1).splitlines(), - six.text_type(dom2).splitlines()))) - standardMsg = self._truncateMessage(standardMsg, diff) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertHTMLNotEqual(self, html1, html2, msg=None): - """Asserts that two HTML snippets are not semantically equivalent.""" - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 == dom2: - standardMsg = '%s == %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertInHTML(self, needle, haystack, count = None, msg_prefix=''): - needle = assert_and_parse_html(self, needle, None, - 'First argument is not valid HTML:') - haystack = assert_and_parse_html(self, haystack, None, - 'Second argument is not valid HTML:') - real_count = haystack.count(needle) - if count is not None: - self.assertEqual(real_count, count, - msg_prefix + "Found %d instances of '%s' in response" - " (expected %d)" % (real_count, needle, count)) - else: - self.assertTrue(real_count != 0, - msg_prefix + "Couldn't find '%s' in response" % needle) - - def assertJSONEqual(self, raw, expected_data, msg=None): - try: - data = json.loads(raw) - except ValueError: - self.fail("First argument is not valid JSON: %r" % raw) - if isinstance(expected_data, six.string_types): - try: - expected_data = json.loads(expected_data) - except ValueError: - self.fail("Second argument is not valid JSON: %r" % expected_data) - self.assertEqual(data, expected_data, msg=msg) - - def assertXMLEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if not result: - standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertXMLNotEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are not semantically equivalent. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if result: - standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - -class TransactionTestCase(SimpleTestCase): - - # The class we'll use for the test client self.client. - # Can be overridden in derived classes. - client_class = Client - - # Subclasses can ask for resetting of auto increment sequence before each - # test case - reset_sequences = False - - def _pre_setup(self): - """Performs any pre-test setup. This includes: - - * Flushing the database. - * If the Test Case class has a 'fixtures' member, installing the - named fixtures. - * If the Test Case class has a 'urls' member, replace the - ROOT_URLCONF with it. - * Clearing the mail test outbox. - """ - self.client = self.client_class() - self._fixture_setup() - self._urlconf_setup() - mail.outbox = [] - - def _databases_names(self, include_mirrors=True): - # If the test case has a multi_db=True flag, act on all databases, - # including mirrors or not. Otherwise, just on the default DB. - if getattr(self, 'multi_db', False): - return [alias for alias in connections - if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] - else: - return [DEFAULT_DB_ALIAS] - - def _reset_sequences(self, db_name): - conn = connections[db_name] - if conn.features.supports_sequence_reset: - sql_list = \ - conn.ops.sequence_reset_by_name_sql(no_style(), - conn.introspection.sequence_list()) - if sql_list: - with transaction.commit_on_success_unless_managed(using=db_name): - cursor = conn.cursor() - for sql in sql_list: - cursor.execute(sql) - - def _fixture_setup(self): - for db_name in self._databases_names(include_mirrors=False): - # Reset sequences - if self.reset_sequences: - self._reset_sequences(db_name) - - if hasattr(self, 'fixtures'): - # We have to use this slightly awkward syntax due to the fact - # that we're using *args and **kwargs together. - call_command('loaddata', *self.fixtures, - **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) - - def _urlconf_setup(self): - set_urlconf(None) - if hasattr(self, 'urls'): - self._old_root_urlconf = settings.ROOT_URLCONF - settings.ROOT_URLCONF = self.urls - clear_url_caches() - - def _post_teardown(self): - """ Performs any post-test things. This includes: - - * Putting back the original ROOT_URLCONF if it was changed. - * Force closing the connection, so that the next test gets - a clean cursor. - """ - self._fixture_teardown() - self._urlconf_teardown() - # Some DB cursors include SQL statements as part of cursor - # creation. If you have a test that does rollback, the effect - # of these statements is lost, which can effect the operation - # of tests (e.g., losing a timezone setting causing objects to - # be created with the wrong time). - # To make sure this doesn't happen, get a clean connection at the - # start of every test. - for conn in connections.all(): - conn.close() - - def _fixture_teardown(self): - for db in self._databases_names(include_mirrors=False): - call_command('flush', verbosity=0, interactive=False, database=db, - skip_validation=True, reset_sequences=False) - - def _urlconf_teardown(self): - set_urlconf(None) - if hasattr(self, '_old_root_urlconf'): - settings.ROOT_URLCONF = self._old_root_urlconf - clear_url_caches() - def assertRedirects(self, response, expected_url, status_code=302, target_status_code=200, host=None, msg_prefix=''): """Asserts that a response redirected to a specific URL, and that the @@ -736,6 +434,83 @@ class TransactionTestCase(SimpleTestCase): self.fail(msg_prefix + "The form '%s' was not used to render the" " response" % form) + def assertFormsetError(self, response, formset, form_index, field, errors, + msg_prefix=''): + """ + Asserts that a formset used to render the response has a specific error. + + For field errors, specify the ``form_index`` and the ``field``. + For non-field errors, specify the ``form_index`` and the ``field`` as + None. + For non-form errors, specify ``form_index`` as None and the ``field`` + as None. + """ + # Add punctuation to msg_prefix + if msg_prefix: + msg_prefix += ": " + + # Put context(s) into a list to simplify processing. + contexts = to_list(response.context) + if not contexts: + self.fail(msg_prefix + 'Response did not use any contexts to ' + 'render the response') + + # Put error(s) into a list to simplify processing. + errors = to_list(errors) + + # Search all contexts for the error. + found_formset = False + for i, context in enumerate(contexts): + if formset not in context: + continue + found_formset = True + for err in errors: + if field is not None: + if field in context[formset].forms[form_index].errors: + field_errors = context[formset].forms[form_index].errors[field] + self.assertTrue(err in field_errors, + msg_prefix + "The field '%s' on formset '%s', " + "form %d in context %d does not contain the " + "error '%s' (actual errors: %s)" % + (field, formset, form_index, i, err, + repr(field_errors))) + elif field in context[formset].forms[form_index].fields: + self.fail(msg_prefix + "The field '%s' " + "on formset '%s', form %d in " + "context %d contains no errors" % + (field, formset, form_index, i)) + else: + self.fail(msg_prefix + "The formset '%s', form %d in " + "context %d does not contain the field '%s'" % + (formset, form_index, i, field)) + elif form_index is not None: + non_field_errors = context[formset].forms[form_index].non_field_errors() + self.assertFalse(len(non_field_errors) == 0, + msg_prefix + "The formset '%s', form %d in " + "context %d does not contain any non-field " + "errors." % (formset, form_index, i)) + self.assertTrue(err in non_field_errors, + msg_prefix + "The formset '%s', form %d " + "in context %d does not contain the " + "non-field error '%s' " + "(actual errors: %s)" % + (formset, form_index, i, err, + repr(non_field_errors))) + else: + non_form_errors = context[formset].non_form_errors() + self.assertFalse(len(non_form_errors) == 0, + msg_prefix + "The formset '%s' in " + "context %d does not contain any " + "non-form errors." % (formset, i)) + self.assertTrue(err in non_form_errors, + msg_prefix + "The formset '%s' in context " + "%d does not contain the " + "non-form error '%s' (actual errors: %s)" % + (formset, i, err, repr(non_form_errors))) + if not found_formset: + self.fail(msg_prefix + "The formset '%s' was not used to render " + "the response" % formset) + def assertTemplateUsed(self, response=None, template_name=None, msg_prefix=''): """ Asserts that the template with the provided name was used in rendering @@ -787,6 +562,236 @@ class TransactionTestCase(SimpleTestCase): msg_prefix + "Template '%s' was used unexpectedly in rendering" " the response" % template_name) + def assertRaisesMessage(self, expected_exception, expected_message, + callable_obj=None, *args, **kwargs): + """ + Asserts that the message in a raised exception matches the passed + value. + + Args: + expected_exception: Exception class expected to be raised. + expected_message: expected error message string value. + callable_obj: Function to be called. + args: Extra args. + kwargs: Extra kwargs. + """ + return six.assertRaisesRegex(self, expected_exception, + re.escape(expected_message), callable_obj, *args, **kwargs) + + def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, + field_kwargs=None, empty_value=''): + """ + Asserts that a form field behaves correctly with various inputs. + + Args: + fieldclass: the class of the field to be tested. + valid: a dictionary mapping valid inputs to their expected + cleaned values. + invalid: a dictionary mapping invalid inputs to one or more + raised error messages. + field_args: the args passed to instantiate the field + field_kwargs: the kwargs passed to instantiate the field + empty_value: the expected clean output for inputs in empty_values + + """ + if field_args is None: + field_args = [] + if field_kwargs is None: + field_kwargs = {} + required = fieldclass(*field_args, **field_kwargs) + optional = fieldclass(*field_args, + **dict(field_kwargs, required=False)) + # test valid inputs + for input, output in valid.items(): + self.assertEqual(required.clean(input), output) + self.assertEqual(optional.clean(input), output) + # test invalid inputs + for input, errors in invalid.items(): + with self.assertRaises(ValidationError) as context_manager: + required.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + + with self.assertRaises(ValidationError) as context_manager: + optional.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + # test required inputs + error_required = [force_text(required.error_messages['required'])] + for e in required.empty_values: + with self.assertRaises(ValidationError) as context_manager: + required.clean(e) + self.assertEqual(context_manager.exception.messages, + error_required) + self.assertEqual(optional.clean(e), empty_value) + # test that max_length and min_length are always accepted + if issubclass(fieldclass, CharField): + field_kwargs.update({'min_length':2, 'max_length':20}) + self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), + fieldclass)) + + def assertHTMLEqual(self, html1, html2, msg=None): + """ + Asserts that two HTML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + """ + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 != dom2: + standardMsg = '%s != %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + diff = ('\n' + '\n'.join(difflib.ndiff( + six.text_type(dom1).splitlines(), + six.text_type(dom2).splitlines()))) + standardMsg = self._truncateMessage(standardMsg, diff) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertHTMLNotEqual(self, html1, html2, msg=None): + """Asserts that two HTML snippets are not semantically equivalent.""" + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 == dom2: + standardMsg = '%s == %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertInHTML(self, needle, haystack, count=None, msg_prefix=''): + needle = assert_and_parse_html(self, needle, None, + 'First argument is not valid HTML:') + haystack = assert_and_parse_html(self, haystack, None, + 'Second argument is not valid HTML:') + real_count = haystack.count(needle) + if count is not None: + self.assertEqual(real_count, count, + msg_prefix + "Found %d instances of '%s' in response" + " (expected %d)" % (real_count, needle, count)) + else: + self.assertTrue(real_count != 0, + msg_prefix + "Couldn't find '%s' in response" % needle) + + def assertJSONEqual(self, raw, expected_data, msg=None): + try: + data = json.loads(raw) + except ValueError: + self.fail("First argument is not valid JSON: %r" % raw) + if isinstance(expected_data, six.string_types): + try: + expected_data = json.loads(expected_data) + except ValueError: + self.fail("Second argument is not valid JSON: %r" % expected_data) + self.assertEqual(data, expected_data, msg=msg) + + def assertXMLEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if not result: + standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertXMLNotEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are not semantically equivalent. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if result: + standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + +class TransactionTestCase(SimpleTestCase): + + # Subclasses can ask for resetting of auto increment sequence before each + # test case + reset_sequences = False + + def _pre_setup(self): + """Performs any pre-test setup. This includes: + + * Flushing the database. + * If the Test Case class has a 'fixtures' member, installing the + named fixtures. + """ + super(TransactionTestCase, self)._pre_setup() + self._fixture_setup() + + def _databases_names(self, include_mirrors=True): + # If the test case has a multi_db=True flag, act on all databases, + # including mirrors or not. Otherwise, just on the default DB. + if getattr(self, 'multi_db', False): + return [alias for alias in connections + if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] + else: + return [DEFAULT_DB_ALIAS] + + def _reset_sequences(self, db_name): + conn = connections[db_name] + if conn.features.supports_sequence_reset: + sql_list = \ + conn.ops.sequence_reset_by_name_sql(no_style(), + conn.introspection.sequence_list()) + if sql_list: + with transaction.commit_on_success_unless_managed(using=db_name): + cursor = conn.cursor() + for sql in sql_list: + cursor.execute(sql) + + def _fixture_setup(self): + for db_name in self._databases_names(include_mirrors=False): + # Reset sequences + if self.reset_sequences: + self._reset_sequences(db_name) + + if hasattr(self, 'fixtures'): + # We have to use this slightly awkward syntax due to the fact + # that we're using *args and **kwargs together. + call_command('loaddata', *self.fixtures, + **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) + + def _post_teardown(self): + """Performs any post-test things. This includes: + + * Putting back the original ROOT_URLCONF if it was changed. + * Force closing the connection, so that the next test gets + a clean cursor. + """ + self._fixture_teardown() + super(TransactionTestCase, self)._post_teardown() + # Some DB cursors include SQL statements as part of cursor + # creation. If you have a test that does rollback, the effect + # of these statements is lost, which can effect the operation + # of tests (e.g., losing a timezone setting causing objects to + # be created with the wrong time). + # To make sure this doesn't happen, get a clean connection at the + # start of every test. + for conn in connections.all(): + conn.close() + + def _fixture_teardown(self): + for db_name in self._databases_names(include_mirrors=False): + call_command('flush', verbosity=0, interactive=False, database=db_name, + skip_validation=True, reset_sequences=False) + def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True): items = six.moves.map(transform, qs) if not ordered: @@ -841,15 +846,19 @@ class TestCase(TransactionTestCase): # Remove this when the legacy transaction management goes away. disable_transaction_methods() - for db in self._databases_names(include_mirrors=False): + for db_name in self._databases_names(include_mirrors=False): if hasattr(self, 'fixtures'): - call_command('loaddata', *self.fixtures, - **{ - 'verbosity': 0, - 'commit': False, - 'database': db, - 'skip_validation': True, - }) + try: + call_command('loaddata', *self.fixtures, + **{ + 'verbosity': 0, + 'commit': False, + 'database': db_name, + 'skip_validation': True, + }) + except Exception: + self._fixture_teardown() + raise def _fixture_teardown(self): if not connections_support_transactions(): diff --git a/django/test/utils.py b/django/test/utils.py index 92cef59f72..be586c75a6 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,4 +1,7 @@ +from contextlib import contextmanager +import logging import re +import sys import warnings from functools import wraps from xml.dom.minidom import parseString, Node @@ -57,6 +60,16 @@ class ContextList(list): return False return True + def keys(self): + """ + Flattened keys of subcontexts. + """ + keys = set() + for subcontext in self: + for dict in subcontext: + keys |= set(dict.keys()) + return keys + def instrumented_test_render(self, context): """ @@ -380,3 +393,41 @@ class CaptureQueriesContext(object): if exc_type is not None: return self.final_queries = len(self.connection.queries) + + +class IgnoreDeprecationWarningsMixin(object): + + warning_class = DeprecationWarning + + def setUp(self): + super(IgnoreDeprecationWarningsMixin, self).setUp() + self.catch_warnings = warnings.catch_warnings() + self.catch_warnings.__enter__() + warnings.filterwarnings("ignore", category=self.warning_class) + + def tearDown(self): + self.catch_warnings.__exit__(*sys.exc_info()) + super(IgnoreDeprecationWarningsMixin, self).tearDown() + + +class IgnorePendingDeprecationWarningsMixin(IgnoreDeprecationWarningsMixin): + + warning_class = PendingDeprecationWarning + + +@contextmanager +def patch_logger(logger_name, log_level): + """ + Context manager that takes a named logger and the logging level + and provides a simple mock-like list of messages received + """ + calls = [] + def replacement(msg): + calls.append(msg) + logger = logging.getLogger(logger_name) + orig = getattr(logger, log_level) + setattr(logger, log_level, replacement) + try: + yield calls + finally: + setattr(logger, log_level, orig) diff --git a/django/utils/_os.py b/django/utils/_os.py index 6c1cd17a83..607e02c94d 100644 --- a/django/utils/_os.py +++ b/django/utils/_os.py @@ -38,7 +38,7 @@ def upath(path): """ Always return a unicode path. """ - if not six.PY3: + if not six.PY3 and not isinstance(path, six.text_type): return path.decode(fs_encoding) return path diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 5d0f381ffa..15db972560 100644 --- a/django/utils/crypto.py +++ b/django/utils/crypto.py @@ -28,10 +28,6 @@ from django.utils import six from django.utils.six.moves import xrange -_trans_5c = bytearray([(x ^ 0x5C) for x in xrange(256)]) -_trans_36 = bytearray([(x ^ 0x36) for x in xrange(256)]) - - def salted_hmac(key_salt, value, secret=None): """ Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a @@ -130,9 +126,9 @@ def _fast_hmac(key, msg, digest): if len(key) > dig1.block_size: key = digest(key).digest() key += b'\x00' * (dig1.block_size - len(key)) - dig1.update(key.translate(_trans_36)) + dig1.update(key.translate(hmac.trans_36)) dig1.update(msg) - dig2.update(key.translate(_trans_5c)) + dig2.update(key.translate(hmac.trans_5C)) dig2.update(dig1.digest()) return dig2 diff --git a/django/utils/functional.py b/django/utils/functional.py index cab74886d3..0606c775ef 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -4,6 +4,7 @@ from functools import wraps import sys from django.utils import six +from django.utils.six.moves import copyreg # You can't trivially replace this with `functools.partial` because this binds @@ -328,15 +329,23 @@ class SimpleLazyObject(LazyObject): self._setup() return self._wrapped.__dict__ - # Python 3.3 will call __reduce__ when pickling; these methods are needed - # to serialize and deserialize correctly. They are not called in earlier - # versions of Python. + # Python 3.3 will call __reduce__ when pickling; this method is needed + # to serialize and deserialize correctly. @classmethod def __newobj__(cls, *args): return cls.__new__(cls, *args) - def __reduce__(self): - return (self.__newobj__, (self.__class__,), self.__getstate__()) + def __reduce_ex__(self, proto): + if proto >= 2: + # On Py3, since the default protocol is 3, pickle uses the + # ``__newobj__`` method (& more efficient opcodes) for writing. + return (self.__newobj__, (self.__class__,), self.__getstate__()) + else: + # On Py2, the default protocol is 0 (for back-compat) & the above + # code fails miserably (see regression test). Instead, we return + # exactly what's returned if there's no ``__reduce__`` method at + # all. + return (copyreg._reconstructor, (self.__class__, object, None), self.__getstate__()) # Return a meaningful representation of the lazy object for debugging # without evaluating the wrapped object. diff --git a/django/utils/html.py b/django/utils/html.py index 8b28d97d13..0d28c77a61 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -16,6 +16,9 @@ from django.utils.functional import allow_lazy from django.utils import six from django.utils.text import normalize_newlines +from .html_parser import HTMLParser, HTMLParseError + + # Configuration for urlize() function. TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)'] WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>')] @@ -33,7 +36,6 @@ link_target_attribute_re = re.compile(r'(<a [^>]*?)target=[^\s>]+') html_gunk_re = re.compile(r'(?:<br clear="all">|<i><\/i>|<b><\/b>|<em><\/em>|<strong><\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE) hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL) trailing_empty_content_re = re.compile(r'(?:<p>(?: |\s|<br \/>)*?</p>\s*)+\Z') -strip_tags_re = re.compile(r'</?\S([^=>]*=(\s*"[^"]*"|\s*\'[^\']*\'|\S*)|[^>])*?>', re.IGNORECASE) def escape(text): @@ -116,9 +118,31 @@ def linebreaks(value, autoescape=False): return '\n\n'.join(paras) linebreaks = allow_lazy(linebreaks, six.text_type) + +class MLStripper(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self.reset() + self.fed = [] + def handle_data(self, d): + self.fed.append(d) + def handle_entityref(self, name): + self.fed.append('&%s;' % name) + def handle_charref(self, name): + self.fed.append('&#%s;' % name) + def get_data(self): + return ''.join(self.fed) + def strip_tags(value): """Returns the given HTML with all tags stripped.""" - return strip_tags_re.sub('', force_text(value)) + s = MLStripper() + try: + s.feed(value) + s.close() + except HTMLParseError: + return value + else: + return s.get_data() strip_tags = allow_lazy(strip_tags) def remove_tags(html, tags): @@ -281,3 +305,10 @@ def clean_html(text): text = trailing_empty_content_re.sub('', text) return text clean_html = allow_lazy(clean_html, six.text_type) + +def avoid_wrapping(value): + """ + Avoid text wrapping in the middle of a phrase by adding non-breaking + spaces where there previously were normal spaces. + """ + return value.replace(" ", "\xa0") diff --git a/django/utils/http.py b/django/utils/http.py index 15fac6bfca..f4911b4ec0 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -71,7 +71,7 @@ urlunquote_plus = allow_lazy(urlunquote_plus, six.text_type) def urlencode(query, doseq=0): """ A version of Python's urllib.urlencode() function that can operate on - unicode strings. The parameters are first case to UTF-8 encoded strings and + unicode strings. The parameters are first cast to UTF-8 encoded strings and then encoded as per normal. """ if isinstance(query, MultiValueDict): @@ -226,7 +226,10 @@ def same_origin(url1, url2): Checks if two URLs are 'same-origin' """ p1, p2 = urllib_parse.urlparse(url1), urllib_parse.urlparse(url2) - return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port) + try: + return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port) + except ValueError: + return False def is_safe_url(url, host=None): """ diff --git a/django/utils/image.py b/django/utils/image.py index 54c11adfee..d251ab9d0b 100644 --- a/django/utils/image.py +++ b/django/utils/image.py @@ -124,7 +124,7 @@ def _detect_image_library(): import _imaging as PIL_imaging except ImportError as err: raise ImproperlyConfigured( - _("The '_imaging' module for the PIL could not be " + + _("The '_imaging' module for the PIL could not be " "imported: %s" % err) ) diff --git a/django/utils/ipv6.py b/django/utils/ipv6.py index 8881574eaa..eaacfb4623 100644 --- a/django/utils/ipv6.py +++ b/django/utils/ipv6.py @@ -138,8 +138,7 @@ def _unpack_ipv4(ip_str): if not ip_str.lower().startswith('0000:0000:0000:0000:0000:ffff:'): return None - hextets = ip_str.split(':') - return hextets[-1] + return ip_str.rsplit(':', 1)[1] def is_valid_ipv6_address(ip_str): """ diff --git a/django/utils/log.py b/django/utils/log.py index a9b62caae1..6734a7261e 100644 --- a/django/utils/log.py +++ b/django/utils/log.py @@ -63,6 +63,11 @@ DEFAULT_LOGGING = { 'level': 'ERROR', 'propagate': False, }, + 'django.security': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': False, + }, 'py.warnings': { 'handlers': ['console'], }, @@ -87,8 +92,8 @@ class AdminEmailHandler(logging.Handler): request = record.request subject = '%s (%s IP): %s' % ( record.levelname, - (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS - and 'internal' or 'EXTERNAL'), + ('internal' if request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS + else 'EXTERNAL'), record.getMessage() ) filter = get_exception_reporter_filter(request) diff --git a/django/utils/safestring.py b/django/utils/safestring.py index 07e0bf4cea..3774012d32 100644 --- a/django/utils/safestring.py +++ b/django/utils/safestring.py @@ -4,7 +4,7 @@ without further escaping in HTML. Marking something as a "safe string" means that the producer of the string has already turned characters that should not be interpreted by the HTML engine (e.g. '<') into the appropriate entities. """ -from django.utils.functional import curry, Promise +from django.utils.functional import curry, Promise, allow_lazy from django.utils import six class EscapeData(object): @@ -14,13 +14,13 @@ class EscapeBytes(bytes, EscapeData): """ A byte string that should be HTML-escaped when output. """ - pass + __new__ = allow_lazy(bytes.__new__, bytes) class EscapeText(six.text_type, EscapeData): """ A unicode string object that should be HTML-escaped when output. """ - pass + __new__ = allow_lazy(six.text_type.__new__, six.text_type) if six.PY3: EscapeString = EscapeText @@ -37,6 +37,8 @@ class SafeBytes(bytes, SafeData): A bytes subclass that has been specifically marked as "safe" (requires no further escaping) for HTML output purposes. """ + __new__ = allow_lazy(bytes.__new__, bytes) + def __add__(self, rhs): """ Concatenating a safe byte string with another safe byte string or safe @@ -69,6 +71,8 @@ class SafeText(six.text_type, SafeData): A unicode (Python 2) / str (Python 3) subclass that has been specifically marked as "safe" for HTML output purposes. """ + __new__ = allow_lazy(six.text_type.__new__, six.text_type) + def __add__(self, rhs): """ Concatenating a safe unicode string with another safe byte string or diff --git a/django/utils/timesince.py b/django/utils/timesince.py index d70ab2ffe1..46c387f262 100644 --- a/django/utils/timesince.py +++ b/django/utils/timesince.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime +from django.utils.html import avoid_wrapping from django.utils.timezone import is_aware, utc from django.utils.translation import ugettext, ungettext_lazy @@ -40,18 +41,18 @@ def timesince(d, now=None, reversed=False): since = delta.days * 24 * 60 * 60 + delta.seconds if since <= 0: # d is in the future compared to now, stop processing. - return ugettext('0 minutes') + return avoid_wrapping(ugettext('0 minutes')) for i, (seconds, name) in enumerate(chunks): count = since // seconds if count != 0: break - result = name % count + result = avoid_wrapping(name % count) if i + 1 < len(chunks): # Now get the second item seconds2, name2 = chunks[i + 1] count2 = (since - (seconds * count)) // seconds2 if count2 != 0: - result += ugettext(', ') + name2 % count2 + result += ugettext(', ') + avoid_wrapping(name2 % count2) return result def timeuntil(d, now=None): diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 07353c35ee..195badfc00 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -10,7 +10,9 @@ from threading import local import warnings from django.utils.importlib import import_module +from django.utils.datastructures import SortedDict from django.utils.encoding import force_str, force_text +from django.utils.functional import memoize from django.utils._os import upath from django.utils.safestring import mark_safe, SafeData from django.utils import six @@ -29,6 +31,7 @@ _default = None # This is a cache for normalized accept-header languages to prevent multiple # file lookups when checking the same locale on repeated requests. _accepted = {} +_checked_languages = {} # magic gettext number to separate context from message CONTEXT_SEPARATOR = "\x04" @@ -77,7 +80,6 @@ class DjangoTranslation(gettext_module.GNUTranslations): def __init__(self, *args, **kw): gettext_module.GNUTranslations.__init__(self, *args, **kw) self.set_output_charset('utf-8') - self.django_output_charset = 'utf-8' self.__language = '??' def merge(self, other): @@ -140,7 +142,7 @@ def translation(language): # doesn't affect en-gb), even though they will both use the core "en" # translation. So we have to subvert Python's internal gettext caching. base_lang = lambda x: x.split('-', 1)[0] - if base_lang(lang) in [base_lang(trans) for trans in _translations]: + if base_lang(lang) in [base_lang(trans) for trans in list(_translations)]: res._info = res._info.copy() res._catalog = res._catalog.copy() @@ -355,34 +357,54 @@ def check_for_language(lang_code): if gettext_module.find('django', path, [to_locale(lang_code)]) is not None: return True return False +check_for_language = memoize(check_for_language, _checked_languages, 1) -def get_supported_language_variant(lang_code, supported=None): +def get_supported_language_variant(lang_code, supported=None, strict=False): """ Returns the language-code that's listed in supported languages, possibly selecting a more generic variant. Raises LookupError if nothing found. + + If `strict` is False (the default), the function will look for an alternative + country-specific variant when the currently checked is not found. """ if supported is None: from django.conf import settings - supported = dict(settings.LANGUAGES) - if lang_code and lang_code not in supported: - lang_code = lang_code.split('-')[0] # e.g. if fr-ca is not supported fallback to fr - if lang_code and lang_code in supported and check_for_language(lang_code): - return lang_code + supported = SortedDict(settings.LANGUAGES) + if lang_code: + # if fr-CA is not supported, try fr-ca; if that fails, fallback to fr. + generic_lang_code = lang_code.split('-')[0] + variants = (lang_code, lang_code.lower(), generic_lang_code, + generic_lang_code.lower()) + for code in variants: + if code in supported and check_for_language(code): + return code + if not strict: + # if fr-fr is not supported, try fr-ca. + for supported_code in supported: + if supported_code.startswith((generic_lang_code + '-', + generic_lang_code.lower() + '-')): + return supported_code raise LookupError(lang_code) -def get_language_from_path(path, supported=None): +def get_language_from_path(path, supported=None, strict=False): """ Returns the language-code if there is a valid language-code found in the `path`. + + If `strict` is False (the default), the function will look for an alternative + country-specific variant when the currently checked is not found. """ if supported is None: from django.conf import settings - supported = dict(settings.LANGUAGES) + supported = SortedDict(settings.LANGUAGES) regex_match = language_code_prefix_re.match(path) - if regex_match: - lang_code = regex_match.group(1) - if lang_code in supported and check_for_language(lang_code): - return lang_code + if not regex_match: + return None + lang_code = regex_match.group(1) + try: + return get_supported_language_variant(lang_code, supported, strict=strict) + except LookupError: + return None def get_language_from_request(request, check_path=False): """ @@ -396,7 +418,7 @@ def get_language_from_request(request, check_path=False): """ global _accepted from django.conf import settings - supported = dict(settings.LANGUAGES) + supported = SortedDict(settings.LANGUAGES) if check_path: lang_code = get_language_from_path(request.path_info, supported) @@ -420,11 +442,6 @@ def get_language_from_request(request, check_path=False): if accept_lang == '*': break - # We have a very restricted form for our language files (no encoding - # specifier, since they all must be UTF-8 and only one possible - # language each time. So we avoid the overhead of gettext.find() and - # work out the MO file manually. - # 'normalized' is the root name of the locale in POSIX format (which is # the format used for the directories holding the MO files). normalized = locale.locale_alias.get(to_locale(accept_lang, True)) @@ -438,14 +455,13 @@ def get_language_from_request(request, check_path=False): # need to check again. return _accepted[normalized] - for lang, dirname in ((accept_lang, normalized), - (accept_lang.split('-')[0], normalized.split('_')[0])): - if lang.lower() not in supported: - continue - for path in all_locale_paths(): - if os.path.exists(os.path.join(path, dirname, 'LC_MESSAGES', 'django.mo')): - _accepted[normalized] = lang - return lang + try: + accept_lang = get_supported_language_variant(accept_lang, supported) + except LookupError: + continue + else: + _accepted[normalized] = accept_lang + return accept_lang try: return get_supported_language_variant(settings.LANGUAGE_CODE, supported) diff --git a/django/views/debug.py b/django/views/debug.py index 9b95b524d2..0458580221 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -218,6 +218,15 @@ class ExceptionReporter(object): self.exc_value = Exception('Deprecated String Exception: %r' % self.exc_type) self.exc_type = type(self.exc_value) + def format_path_status(self, path): + if not os.path.exists(path): + return "File does not exist" + if not os.path.isfile(path): + return "Not a file" + if not os.access(path, os.R_OK): + return "File is not readable" + return "File exists" + def get_traceback_data(self): "Return a Context instance containing traceback information." @@ -230,8 +239,10 @@ class ExceptionReporter(object): source_list_func = loader.get_template_sources # NOTE: This assumes exc_value is the name of the template that # the loader attempted to load. - template_list = [{'name': t, 'exists': os.path.exists(t)} \ - for t in source_list_func(str(self.exc_value))] + template_list = [{ + 'name': t, + 'status': self.format_path_status(t), + } for t in source_list_func(str(self.exc_value))] except AttributeError: template_list = [] loader_name = loader.__module__ + '.' + loader.__class__.__name__ @@ -347,7 +358,7 @@ class ExceptionReporter(object): if source is None: try: with open(filename, 'rb') as fp: - source = fp.readlines() + source = fp.read().splitlines() except (OSError, IOError): pass if source is None: @@ -370,9 +381,9 @@ class ExceptionReporter(object): lower_bound = max(0, lineno - context_lines) upper_bound = lineno + context_lines - pre_context = [line.strip('\n') for line in source[lower_bound:lineno]] - context_line = source[lineno].strip('\n') - post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]] + pre_context = source[lower_bound:lineno] + context_line = source[lineno] + post_context = source[lineno+1:upper_bound] return lower_bound, pre_context, context_line, post_context @@ -394,7 +405,7 @@ class ExceptionReporter(object): if pre_context_lineno is not None: frames.append({ 'tb': tb, - 'type': module_name.startswith('django.') and 'django' or 'user', + 'type': 'django' if module_name.startswith('django.') else 'user', 'filename': filename, 'function': function, 'lineno': lineno + 1, @@ -584,7 +595,7 @@ TECHNICAL_500_TEMPLATE = """ <body> <div id="summary"> <h1>{% if exception_type %}{{ exception_type }}{% else %}Report{% endif %}{% if request %} at {{ request.path_info|escape }}{% endif %}</h1> - <pre class="exception_value">{% if exception_value %}{{ exception_value|force_escape }}{% else %}No exception supplied{% endif %}</pre> + <pre class="exception_value">{% if exception_value %}{{ exception_value|force_escape }}{% else %}No exception message supplied{% endif %}</pre> <table class="meta"> {% if request %} <tr> @@ -650,7 +661,9 @@ TECHNICAL_500_TEMPLATE = """ <ul> {% for loader in loader_debug_info %} <li>Using loader <code>{{ loader.loader }}</code>: - <ul>{% for t in loader.templates %}<li><code>{{ t.name }}</code> (File {% if t.exists %}exists{% else %}does not exist{% endif %})</li>{% endfor %}</ul> + <ul> + {% for t in loader.templates %}<li><code>{{ t.name }}</code> ({{ t.status }})</li>{% endfor %} + </ul> </li> {% endfor %} </ul> @@ -753,7 +766,7 @@ Installed Middleware: {% if template_does_not_exist %}Template Loader Error: {% if loader_debug_info %}Django tried loading these templates, in this order: {% for loader in loader_debug_info %}Using loader {{ loader.loader }}: -{% for t in loader.templates %}{{ t.name }} (File {% if t.exists %}exists{% else %}does not exist{% endif %}) +{% for t in loader.templates %}{{ t.name }} ({{ t.status }}) {% endfor %}{% endfor %} {% else %}Django couldn't find any templates because your TEMPLATE_LOADERS setting is empty! {% endif %} @@ -927,7 +940,7 @@ Exception Value: {{ exception_value|force_escape }} """ TECHNICAL_500_TEXT_TEMPLATE = """{% load firstof from future %}{% firstof exception_type 'Report' %}{% if request %} at {{ request.path_info }}{% endif %} -{% firstof exception_value 'No exception supplied' %} +{% firstof exception_value 'No exception message supplied' %} {% if request %} Request Method: {{ request.META.REQUEST_METHOD }} Request URL: {{ request.build_absolute_uri }}{% endif %} @@ -943,7 +956,7 @@ Installed Middleware: {% if template_does_not_exist %}Template loader Error: {% if loader_debug_info %}Django tried loading these templates, in this order: {% for loader in loader_debug_info %}Using loader {{ loader.loader }}: -{% for t in loader.templates %}{{ t.name }} (File {% if t.exists %}exists{% else %}does not exist{% endif %}) +{% for t in loader.templates %}{{ t.name }} ({{ t.status }}) {% endfor %}{% endfor %} {% else %}Django couldn't find any templates because your TEMPLATE_LOADERS setting is empty! {% endif %} diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py index 7a7eb6bba6..a6bd7d8526 100644 --- a/django/views/decorators/csrf.py +++ b/django/views/decorators/csrf.py @@ -15,7 +15,7 @@ using the decorator multiple times, is harmless and efficient. class _EnsureCsrfToken(CsrfViewMiddleware): # We need this to behave just like the CsrfViewMiddleware, but not reject - # requests. + # requests or log warnings. def _reject(self, request, reason): return None diff --git a/django/views/defaults.py b/django/views/defaults.py index 89228c50c9..c8a62fc753 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -43,6 +43,21 @@ def server_error(request, template_name='500.html'): return http.HttpResponseServerError(template.render(Context({}))) +@requires_csrf_token +def bad_request(request, template_name='400.html'): + """ + 400 error handler. + + Templates: :template:`400.html` + Context: None + """ + try: + template = loader.get_template(template_name) + except TemplateDoesNotExist: + return http.HttpResponseBadRequest('<h1>Bad Request (400)</h1>') + return http.HttpResponseBadRequest(template.render(Context({}))) + + # This can be called when CsrfViewMiddleware.process_view has not run, # therefore need @requires_csrf_token in case the template needs # {% csrf_token %}. diff --git a/django/views/generic/base.py b/django/views/generic/base.py index d50d6bbc55..286a18d0f2 100644 --- a/django/views/generic/base.py +++ b/django/views/generic/base.py @@ -30,7 +30,7 @@ class View(object): dispatch-by-method and simple sanity checking. """ - http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] def __init__(self, **kwargs): """ @@ -206,3 +206,6 @@ class RedirectView(View): def put(self, request, *args, **kwargs): return self.get(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.get(request, *args, **kwargs) diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index 58302bbe23..23000641b4 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -93,9 +93,11 @@ class SingleObjectMixin(ContextMixin): Insert the single object into the context dict. """ context = {} - context_object_name = self.get_context_object_name(self.object) - if context_object_name: - context[context_object_name] = self.object + if self.object: + context['object'] = self.object + context_object_name = self.get_context_object_name(self.object) + if context_object_name: + context[context_object_name] = self.object context.update(kwargs) return super(SingleObjectMixin, self).get_context_data(**context) @@ -122,7 +124,7 @@ class SingleObjectTemplateResponseMixin(TemplateResponseMixin): * the value of ``template_name`` on the view (if provided) * the contents of the ``template_name_field`` field on the object instance that the view is operating upon (if available) - * ``<app_label>/<object_name><template_name_suffix>.html`` + * ``<app_label>/<object_name><template_name_suffix>.html`` """ try: names = super(SingleObjectTemplateResponseMixin, self).get_template_names() diff --git a/django/views/generic/edit.py b/django/views/generic/edit.py index e2cc741ffb..cf87aeed27 100644 --- a/django/views/generic/edit.py +++ b/django/views/generic/edit.py @@ -136,20 +136,6 @@ class ModelFormMixin(FormMixin, SingleObjectMixin): self.object = form.save() return super(ModelFormMixin, self).form_valid(form) - def get_context_data(self, **kwargs): - """ - If an object has been supplied, inject it into the context with the - supplied context_object_name name. - """ - context = {} - if self.object: - context['object'] = self.object - context_object_name = self.get_context_object_name(self.object) - if context_object_name: - context[context_object_name] = self.object - context.update(kwargs) - return super(ModelFormMixin, self).get_context_data(**context) - class ProcessFormView(View): """ diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 08c4bbcda0..1aff3454f4 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -105,7 +105,7 @@ class MultipleObjectMixin(ContextMixin): """ Get the context for this view. """ - queryset = kwargs.pop('object_list') + queryset = kwargs.pop('object_list', self.object_list) page_size = self.get_paginate_by(queryset) context_object_name = self.get_context_object_name(queryset) if page_size: @@ -149,7 +149,7 @@ class BaseListView(MultipleObjectMixin, View): if is_empty: raise Http404(_("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) + context = self.get_context_data() return self.render_to_response(context) diff --git a/django/views/i18n.py b/django/views/i18n.py index 37ec10b552..71ac005855 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -184,38 +184,8 @@ def render_javascript_catalog(catalog=None, plural=None): return http.HttpResponse(template.render(context), 'text/javascript') -def null_javascript_catalog(request, domain=None, packages=None): - """ - Returns "identity" versions of the JavaScript i18n functions -- i.e., - versions that don't actually do anything. - """ - return render_javascript_catalog() - - -def javascript_catalog(request, domain='djangojs', packages=None): - """ - Returns the selected language catalog as a javascript library. - - Receives the list of packages to check for translations in the - packages parameter either from an infodict or as a +-delimited - string from the request. Default is 'django.conf'. - - Additionally you can override the gettext domain for this view, - but usually you don't want to do that, as JavaScript messages - go to the djangojs domain. But this might be needed if you - deliver your JavaScript source from Django templates. - """ +def get_javascript_catalog(locale, domain, packages): default_locale = to_locale(settings.LANGUAGE_CODE) - locale = to_locale(get_language()) - - if request.GET and 'language' in request.GET: - if check_for_language(request.GET['language']): - locale = to_locale(request.GET['language']) - - if packages is None: - packages = ['django.conf'] - if isinstance(packages, six.string_types): - packages = packages.split('+') packages = [p for p in packages if p == 'django.conf' or p in settings.INSTALLED_APPS] t = {} paths = [] @@ -296,4 +266,40 @@ def javascript_catalog(request, domain='djangojs', packages=None): for k, v in pdict.items(): catalog[k] = [v.get(i, '') for i in range(maxcnts[msgid] + 1)] + return catalog, plural + + +def null_javascript_catalog(request, domain=None, packages=None): + """ + Returns "identity" versions of the JavaScript i18n functions -- i.e., + versions that don't actually do anything. + """ + return render_javascript_catalog() + + +def javascript_catalog(request, domain='djangojs', packages=None): + """ + Returns the selected language catalog as a javascript library. + + Receives the list of packages to check for translations in the + packages parameter either from an infodict or as a +-delimited + string from the request. Default is 'django.conf'. + + Additionally you can override the gettext domain for this view, + but usually you don't want to do that, as JavaScript messages + go to the djangojs domain. But this might be needed if you + deliver your JavaScript source from Django templates. + """ + locale = to_locale(get_language()) + + if request.GET and 'language' in request.GET: + if check_for_language(request.GET['language']): + locale = to_locale(request.GET['language']) + + if packages is None: + packages = ['django.conf'] + if isinstance(packages, six.string_types): + packages = packages.split('+') + + catalog, plural = get_javascript_catalog(locale, domain, packages) return render_javascript_catalog(catalog, plural) diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py index 3ae770da20..833232a0c3 100644 --- a/docs/_ext/djangodocs.py +++ b/docs/_ext/djangodocs.py @@ -137,7 +137,7 @@ class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): ) title = "%s%s" % ( self.version_text[node['type']] % node['version'], - len(node) and ":" or "." + ":" if len(node) else "." ) self.body.append('<span class="title">%s</span> ' % title) diff --git a/docs/conf.py b/docs/conf.py index a654e3c4d6..a01ddb60b8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -109,7 +109,7 @@ pygments_style = 'trac' intersphinx_mapping = { 'python': ('http://docs.python.org/2.7', None), 'sphinx': ('http://sphinx.pocoo.org/', None), - 'six': ('http://packages.python.org/six/', None), + 'six': ('http://pythonhosted.org/six/', None), 'simplejson': ('http://simplejson.readthedocs.org/en/latest/', None), } diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index 1d9a7c7427..ec40754094 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -27,12 +27,6 @@ account has :attr:`~django.contrib.auth.models.User.is_active` and :attr:`~django.contrib.auth.models.User.is_staff` set to True. The admin site only allows access to users with those two fields both set to True. -How can I prevent the cache middleware from caching the admin site? -------------------------------------------------------------------- - -Set the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` setting to ``True``. See the -:doc:`cache documentation </topics/cache>` for more information. - How do I automatically set a field's value to the user who last edited the object in the admin? ----------------------------------------------------------------------------------------------- diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 0d35654a04..35f78718e4 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -20,7 +20,8 @@ create a new app to hold them. The app should contain a ``templatetags`` directory, at the same level as ``models.py``, ``views.py``, etc. If this doesn't already exist, create it - don't forget the ``__init__.py`` file to ensure the directory is treated as a -Python package. +Python package. After adding this module, you will need to restart your server +before you can use the tags or filters in templates. Your custom tags and filters will live in a module inside the ``templatetags`` directory. The name of the module file is the name you'll use to load the tags @@ -72,6 +73,8 @@ following: For more information on the :ttag:`load` tag, read its documentation. +.. _howto-writing-custom-template-filters: + Writing custom template filters ------------------------------- @@ -300,18 +303,21 @@ Template filter code falls into one of two situations: .. code-block:: python - from django.utils.html import conditional_escape - from django.utils.safestring import mark_safe + from django import template + from django.utils.html import conditional_escape + from django.utils.safestring import mark_safe - @register.filter(needs_autoescape=True) - def initial_letter_filter(text, autoescape=None): - first, other = text[0], text[1:] - if autoescape: - esc = conditional_escape - else: - esc = lambda x: x - result = '<strong>%s</strong>%s' % (esc(first), esc(other)) - return mark_safe(result) + register = template.Library() + + @register.filter(needs_autoescape=True) + def initial_letter_filter(text, autoescape=None): + first, other = text[0], text[1:] + if autoescape: + esc = conditional_escape + else: + esc = lambda x: x + result = '<strong>%s</strong>%s' % (esc(first), esc(other)) + return mark_safe(result) The ``needs_autoescape`` flag and the ``autoescape`` keyword argument mean that our function will know whether automatic escaping is in effect when the @@ -454,8 +460,9 @@ Continuing the above example, we need to define ``CurrentTimeNode``: .. code-block:: python - from django import template import datetime + from django import template + class CurrentTimeNode(template.Node): def __init__(self, format_string): self.format_string = format_string @@ -498,6 +505,8 @@ The ``__init__`` method for the ``Context`` class takes a parameter called .. code-block:: python + from django.template import Context + def render(self, context): # ... new_context = Context({'var': obj}, autoescape=context.autoescape) @@ -545,7 +554,10 @@ A naive implementation of ``CycleNode`` might look something like this: .. code-block:: python - class CycleNode(Node): + import itertools + from django import template + + class CycleNode(template.Node): def __init__(self, cyclevars): self.cycle_iter = itertools.cycle(cyclevars) def render(self, context): @@ -576,7 +588,7 @@ Let's refactor our ``CycleNode`` implementation to use the ``render_context``: .. code-block:: python - class CycleNode(Node): + class CycleNode(template.Node): def __init__(self, cyclevars): self.cyclevars = cyclevars def render(self, context): @@ -664,6 +676,7 @@ Now your tag should begin to look like this: .. code-block:: python from django import template + def do_format_time(parser, token): try: # split_contents() knows not to split quoted strings. @@ -722,6 +735,11 @@ Our earlier ``current_time`` function could thus be written like this: .. code-block:: python + import datetime + from django import template + + register = template.Library() + def current_time(format_string): return datetime.datetime.now().strftime(format_string) @@ -965,6 +983,9 @@ outputting it: .. code-block:: python + import datetime + from django import template + class CurrentTimeNode2(template.Node): def __init__(self, format_string): self.format_string = format_string diff --git a/docs/howto/deployment/checklist.txt b/docs/howto/deployment/checklist.txt index b72be75497..1a235673e8 100644 --- a/docs/howto/deployment/checklist.txt +++ b/docs/howto/deployment/checklist.txt @@ -160,9 +160,10 @@ only useful in development. In addition, you can tune the following settings. :setting:`CONN_MAX_AGE` ----------------------- -Enabling `persistent database connections <persistent-database-connections>`_ -can result in a nice speed-up when connecting to the database accounts for a -significant part of the request processing time. +Enabling :ref:`persistent database connections +<persistent-database-connections>` can result in a nice speed-up when +connecting to the database accounts for a significant part of the request +processing time. This helps a lot on virtualized hosts with limited network performance. @@ -212,3 +213,18 @@ Miscellaneous -------------------------------- This setting is required if you're using the :ttag:`ssi` template tag. + +Python Options +============== + +If you're using Python 2.6.8+, it's strongly recommended that you invoke the +Python process running your Django application using the `-R`_ option or with +the :envvar:`PYTHONHASHSEED` environment variable set to ``random``. + +These options help protect your site from denial-of-service (DoS) +attacks triggered by carefully crafted inputs. Such an attack can +drastically increase CPU usage by causing worst-case performance when +creating ``dict`` instances. See `oCERT advisory #2011-003 +<http://www.ocert.org/advisories/ocert-2011-003.html>`_ for more information. + +.. _-r: http://docs.python.org/2.7/using/cmdline.html#cmdoption-R diff --git a/docs/howto/deployment/wsgi/uwsgi.txt b/docs/howto/deployment/wsgi/uwsgi.txt index 5b40d5f2f7..22f39342d6 100644 --- a/docs/howto/deployment/wsgi/uwsgi.txt +++ b/docs/howto/deployment/wsgi/uwsgi.txt @@ -62,7 +62,6 @@ Here's an example command to start a uWSGI server:: --processes=5 \ # number of worker processes --uid=1000 --gid=2000 \ # if root, uwsgi can drop privileges --harakiri=20 \ # respawn processes taking more than 20 seconds - --limit-as=128 \ # limit the project to 128 MB --max-requests=5000 \ # respawn processes after serving 5000 requests --vacuum \ # clear environment on exit --home=/path/to/virtual/env \ # optional path to a virtualenv diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 27f11f4936..987a503e95 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -98,6 +98,11 @@ crawlers often request:: (Note that these are regular expressions, so we put a backslash in front of periods to escape them.) +If you'd like to customize the behavior of +:class:`django.middleware.common.BrokenLinkEmailsMiddleware` further (for +example to ignore requests coming from web crawlers), you should subclass it +and override its methods. + .. seealso:: 404 errors are logged using the logging framework. By default, these log diff --git a/docs/howto/index.txt b/docs/howto/index.txt index 9d5b067a82..fe25c11756 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -15,6 +15,7 @@ you quickly accomplish common tasks. custom-template-tags custom-file-storage deployment/index + upgrade-version error-reporting initial-data jython diff --git a/docs/howto/static-files/index.txt b/docs/howto/static-files/index.txt index 1fdad94143..3668c5dc41 100644 --- a/docs/howto/static-files/index.txt +++ b/docs/howto/static-files/index.txt @@ -35,8 +35,20 @@ Configuring static files 4. Store your static files in a folder called ``static`` in your app. For example ``my_app/static/my_app/myimage.jpg``. -Now, if you use ``./manage.py runserver``, all static files should be served -automatically at the :setting:`STATIC_URL` and be shown correctly. +.. admonition:: Serving the files + + In addition to these configuration steps, you'll also need to actually + serve the static files. + + During development, this will be done automatically if you use + :djadmin:`runserver` and :setting:`DEBUG` is set to ``True`` (see + :func:`django.contrib.staticfiles.views.serve`). + + This method is **grossly inefficient** and probably **insecure**, + so it is **unsuitable for production**. + + See :doc:`/howto/static-files/deployment` for proper strategies to serve + static files in production environments. Your project will probably also have static assets that aren't tied to a particular app. In addition to using a ``static/`` directory inside your apps, diff --git a/docs/howto/upgrade-version.txt b/docs/howto/upgrade-version.txt new file mode 100644 index 0000000000..8777f433f9 --- /dev/null +++ b/docs/howto/upgrade-version.txt @@ -0,0 +1,91 @@ +=================================== +Upgrading Django to a newer version +=================================== + +While it can be a complex process at times, upgrading to the latest Django +version has several benefits: + +* New features and improvements are added. +* Bugs are fixed. +* Older version of Django will eventually no longer receive security updates. + (see :ref:`backwards-compatibility-policy`). +* Upgrading as each new Django release is available makes future upgrades less + painful by keeping your code base up to date. + +Here are some things to consider to help make your upgrade process as smooth as +possible. + +Required Reading +================ + +If it's your first time doing an upgrade, it is useful to read the :doc:`guide +on the different release processes </internals/release-process>`. + +Afterwards, you should familiarize yourself with the changes that were made in +the new Django version(s): + +* Read the :doc:`release notes </releases/index>` for each 'final' release from + the one after your current Django version, up to and including the version to + which you plan to upgrade. +* Look at the :doc:`deprecation timeline</internals/deprecation>` for the + relevant versions. + +Pay particular attention to backwards incompatible changes to get a clear idea +of what will be needed for a successful upgrade. + +Dependencies +============ + +In most cases it will be necessary to upgrade to the latest version of your +Django-related dependencies as well. If the Django version was recently +released or if some of your dependencies are not well-maintained, some of your +dependencies may not yet support the new Django version. In these cases you may +have to wait until new versions of your dependencies are released. + +Installation +============ + +Once you're ready, it is time to :doc:`install the new Django version +</topics/install>`. If you are using virtualenv_ and it is a major upgrade, you +might want to set up a new environment will all the dependencies first. + +Exactly which steps you will need to take depends on your installation process. +The most convenient way is to use pip_ with the ``--upgrade`` or ``-U`` flag: + +.. code-block:: bash + + pip install -U Django + +pip_ also automatically uninstalls the previous version of Django. + +If you use some other installation process, you might have to manually +:ref:`uninstall the old Django version <removing-old-versions-of-django>` and +should look at the complete installation instructions. + +.. _pip: http://www.pip-installer.org/ +.. _virtualenv: http://www.virtualenv.org/ + +Testing +======= + +When the new environment is set up, :doc:`run the full test suite +</topics/testing/overview>` for your application. In Python 2.7+, deprecation +warnings are silenced by default. It is useful to turn the warnings on so they +are shown in the test output (you can also use the flag if you test your app +manually using ``manage.py runserver``): + +.. code-block:: bash + + python -Wall manage.py test + +After you have run the tests, fix any failures. While you have the release +notes fresh in your mind, it may also be a good time to take advantage of new +features in Django by refactoring your code to eliminate any deprecation +warnings. + +Deployment +========== + +When you are sufficiently confident your app works with the new version of +Django, you're ready to go ahead and :doc:`deploy </howto/deployment/index>` +your upgraded Django project. diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 6b9c7df14a..b56c8e469b 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -53,6 +53,7 @@ Journal-World`_ of Lawrence, Kansas, USA. .. _revolution systems: http://revsys.com/ .. _wilson miner: http://wilsonminer.com/ .. _heroku: http://heroku.com/ +.. _Rdio: http://rdio.com Current developers ================== @@ -88,6 +89,23 @@ Malcolm Tredinnick *Malcolm passed away on March 17, 2013.* +`Luke Plant`_ + At University Luke studied physics and Materials Science and also + met `Michael Meeks`_ who introduced him to Linux and Open Source, + re-igniting an interest in programming. Since then he has + contributed to a number of Open Source projects and worked + professionally as a developer. + + Luke has contributed many excellent improvements to Django, + including database-level improvements, the CSRF middleware and + many unit tests. + + Luke currently works for a church in Bradford, UK, and part-time + as a freelance developer. + +.. _luke plant: http://lukeplant.me.uk/ +.. _michael meeks: http://en.wikipedia.org/wiki/Michael_Meeks_(software) + `Russell Keith-Magee`_ Russell studied physics as an undergraduate, and studied neural networks for his PhD. His first job was with a startup in the defense industry developing @@ -102,6 +120,42 @@ Malcolm Tredinnick .. _russell keith-magee: http://cecinestpasun.com/ +`James Bennett`_ + James is Django's release manager, and also contributes to the + documentation and provide the occasional bugfix. + + James came to Web development from philosophy when he discovered + that programmers get to argue just as much while collecting much + better pay. He lives in Lawrence, Kansas and previously worked at + World Online; currently, he's part of the Web development team at + Mozilla. + + He `keeps a blog`_, and enjoys fine port and talking to his car. + +.. _james bennett: http://b-list.org/ +.. _keeps a blog: `james bennett`_ + +`Gary Wilson`_ + Gary starting contributing patches to Django in 2006 while developing Web + applications for `The University of Texas`_ (UT). Since, he has made + contributions to the email and forms systems, as well as many other + improvements and code cleanups throughout the code base. + + Gary is currently a developer and software engineering graduate student at + UT, where his dedication to spreading the ways of Python and Django never + ceases. + + Gary lives in Austin, Texas, USA. + +.. _Gary Wilson: http://thegarywilson.com/ +.. _The University of Texas: http://www.utexas.edu/ + +Matt Boersma + Matt is responsible for Django's Oracle support. + +Ian Kelly + Ian is also responsible for Django's support for Oracle. + Joseph Kocherhans Joseph was the director of lead development at EveryBlock and previously developed at the Lawrence Journal-World. He is treasurer of the `Django @@ -119,28 +173,11 @@ Joseph Kocherhans .. _django software foundation: https://www.djangoproject.com/foundation/ .. _charango: http://en.wikipedia.org/wiki/Charango -`Luke Plant`_ - At University Luke studied physics and Materials Science and also - met `Michael Meeks`_ who introduced him to Linux and Open Source, - re-igniting an interest in programming. Since then he has - contributed to a number of Open Source projects and worked - professionally as a developer. - - Luke has contributed many excellent improvements to Django, - including database-level improvements, the CSRF middleware and - many unit tests. - - Luke currently works for a church in Bradford, UK, and part-time - as a freelance developer. - -.. _luke plant: http://lukeplant.me.uk/ -.. _michael meeks: http://en.wikipedia.org/wiki/Michael_Meeks_(software) - `Brian Rosner`_ - Brian is currently the tech lead at Eldarion_ managing and developing + Brian is the Chief Architect at Eldarion_ managing and developing Django / Pinax_ based Web sites. He enjoys learning more about programming languages and system architectures and contributing to open source - projects. Brian is the host of the `Django Dose`_ podcasts. + projects. Brian helped immensely in getting Django's "newforms-admin" branch finished in time for Django 1.0; he's now a full committer, continuing to improve on @@ -150,24 +187,8 @@ Joseph Kocherhans .. _brian rosner: http://brosner.com/ .. _eldarion: http://eldarion.com/ -.. _django dose: http://djangodose.com/ .. _pinax: http://pinaxproject.com/ -`Gary Wilson`_ - Gary starting contributing patches to Django in 2006 while developing Web - applications for `The University of Texas`_ (UT). Since, he has made - contributions to the email and forms systems, as well as many other - improvements and code cleanups throughout the code base. - - Gary is currently a developer and software engineering graduate student at - UT, where his dedication to spreading the ways of Python and Django never - ceases. - - Gary lives in Austin, Texas, USA. - -.. _Gary Wilson: http://thegarywilson.com/ -.. _The University of Texas: http://www.utexas.edu/ - Justin Bronn Justin Bronn is a computer scientist and attorney specializing in legal topics related to intellectual property and spatial law. @@ -222,7 +243,7 @@ Karen Tracey .. _James Tauber: http://jtauber.com/ `Alex Gaynor`_ - Alex is a software engineer working at Rdio_. He found Django in 2007 and + Alex is a software engineer working at Rackspace_. He found Django in 2007 and has been addicted ever since he found out you don't need to write out your forms by hand. He has a small obsession with compilers. He's contributed to the ORM, forms, admin, and other components of Django. @@ -230,7 +251,16 @@ Karen Tracey Alex lives in San Francisco, CA, USA. .. _Alex Gaynor: http://alexgaynor.net -.. _Rdio: http://rdio.com +.. _Rackspace: http://www.rackspace.com + +`Simon Meers`_ + Simon discovered Django 0.96 during his Computer Science PhD research and + has been developing with it full-time ever since. His core code + contributions are mostly in Django's admin application. + + Simon works as a freelance developer based in Wollongong, Australia. + +.. _Simon Meers: http://simonmeers.com/ `Andrew Godwin`_ Andrew is a freelance Python developer and tinkerer, and has been @@ -265,6 +295,18 @@ Ramiro Morales Ramiro lives in Córdoba, Argentina. +`Gabriel Hurley`_ + Gabriel has been working with Django since 2008, shortly after the 1.0 + release. Convinced by his business partner that Python and Django were the + right direction for the company, he couldn't have been more happy with the + decision. His contributions range across many areas in Django, but years of + copy-editing and an eye for detail lead him to be particularly at home + while working on Django's documentation. + + Gabriel works as a web developer in Berkeley, CA, USA. + +.. _gabriel hurley: http://strikeawe.com/ + `Chris Beaven`_ Chris has been submitting patches and suggesting crazy ideas for Django since early 2006. An advocate for community involvement and a long-term @@ -290,6 +332,13 @@ Honza Král .. _Whiskey Media: http://www.whiskeymedia.com/ +Tim Graham + When exploring Web frameworks for an independent study project in the fall + of 2008, Tim discovered Django and was lured to it by the documentation. + He enjoys contributing to the docs because they're awesome. + + Tim works as a software engineer and lives in Philadelphia, PA, USA. + `Idan Gazit`_ As a self-professed design geek, Idan was initially attracted to Django sometime between magic-removal and queryset-refactor. Formally trained @@ -439,6 +488,18 @@ Jeremy Dunck .. _Ultimate Frisbee: http://www.montrealultimate.ca .. _Reptiletech: http://www.reptiletech.com +Donald Stufft + Donald found Python and Django in 2007 while trying to find a language, + and web framework that he really enjoyed using after many years of PHP. He + fell in love with the beauty of Python and the way Django made tasks simple + and easy. His contributions to Django focus primarily on ensuring that it + is and remains a secure web framework. + + Donald currently works at `Nebula Inc`_ as a Software Engineer for their + security team and lives in the Greater Philadelphia Area. + +.. _Nebula Inc: https://www.nebula.com/ + `Daniel Lindsley`_ Pythonista since 2003, Djangonaut since 2006. Daniel started with Django just after the v0.90 release (back when ``Manipulators`` looked good) & fell @@ -453,56 +514,6 @@ Jeremy Dunck .. _`Daniel Lindsley`: http://toastdriven.com/ .. _`Amazon Web Services`: https://aws.amazon.com/ -`James Bennett`_ - James is Django's release manager, and also contributes to the - documentation and provide the occasional bugfix. - - James came to Web development from philosophy when he discovered - that programmers get to argue just as much while collecting much - better pay. He lives in Lawrence, Kansas and previously worked at - World Online; currently, he's part of the Web development team at - Mozilla. - - He `keeps a blog`_, and enjoys fine port and talking to his car. - -.. _james bennett: http://b-list.org/ -.. _keeps a blog: `james bennett`_ - -Ian Kelly - Ian is responsible for Django's support for Oracle. - -Matt Boersma - Matt is also responsible for Django's Oracle support. - -`Simon Meers`_ - Simon discovered Django 0.96 during his Computer Science PhD research and - has been developing with it full-time ever since. His core code - contributions are mostly in Django's admin application. He is also helping - to improve Django's documentation. - - Simon works as a freelance developer based in Wollongong, Australia. - -.. _simon meers: http://simonmeers.com/ - -`Gabriel Hurley`_ - Gabriel has been working with Django since 2008, shortly after the 1.0 - release. Convinced by his business partner that Python and Django were the - right direction for the company, he couldn't have been more happy with the - decision. His contributions range across many areas in Django, but years of - copy-editing and an eye for detail lead him to be particularly at home - while working on Django's documentation. - - Gabriel works as a web developer in Berkeley, CA, USA. - -.. _gabriel hurley: http://strikeawe.com/ - -Tim Graham - When exploring Web frameworks for an independent study project in the fall - of 2008, Tim discovered Django and was lured to it by the documentation. - He enjoys contributing to the docs because they're awesome. - - Tim works as a software engineer and lives in Philadelphia, PA, USA. - Marc Tamlyn Marc started life on the web using Django 1.2 back in 2010, and has never looked back. He was involved with rewriting the class based view @@ -515,18 +526,6 @@ Marc Tamlyn .. _CCBV: http://ccbv.co.uk/ .. _Incuna Ltd: http://incuna.com/ -Donald Stufft - Donald found Python and Django in 2007 while trying to find a language, - and web framework that he really enjoyed using after many years of PHP. He - fell in love with the beauty of Python and the way Django made tasks simple - and easy. His contributions to Django focus primarily on ensuring that it - is and remains a secure web framework. - - Donald currently works at `Nebula Inc`_ as a Software Engineer for their - security team and lives in the Greater Philadelphia Area. - -.. _Nebula Inc: https://www.nebula.com/ - Developers Emeritus =================== diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index bc6148ca46..43b799ed51 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -349,8 +349,9 @@ Then, you can help out by: * Closing "Unreviewed" tickets as "invalid", "worksforme" or "duplicate." -* Closing "Unreviewed" tickets as "needsinfo" when they're feature requests - requiring a discussion on `django-developers`_. +* Closing "Unreviewed" tickets as "needsinfo" when the description is too + sparse to be actionnable, or when they're feature requests requiring a + discussion on `django-developers`_. * Correcting the "Needs tests", "Needs documentation", or "Has patch" flags for tickets where they are incorrectly set. diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index f56bf1cdeb..0737b84888 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -27,15 +27,13 @@ Quickstart Running the tests requires a Django settings module that defines the databases to use. To make it easy to get started, Django provides a sample settings module that uses the SQLite database. To run the tests -with this sample ``settings`` module, ``cd`` into the Django -``tests/`` directory and run: +with this sample ``settings`` module: .. code-block:: bash - ./runtests.py --settings=test_sqlite - -If you get an ``ImportError: No module named django.contrib`` error, -you need to add your install of Django to your ``PYTHONPATH``. + git clone git@github.com:django/django.git django-repo + cd django-repo/tests + PYTHONPATH=..:$PYTHONPATH python ./runtests.py --settings=test_sqlite .. _running-unit-tests-settings: @@ -47,14 +45,10 @@ SQLite. If you want to test behavior using a different database (and if you're proposing patches for Django, it's a good idea to test across databases), you may need to define your own settings file. -To run the tests with different settings, ``cd`` to the ``tests/`` directory -and type: +To run the tests with different settings, ensure that the module is on your +``PYTHONPATH`` and pass the module with ``--settings``. -.. code-block:: bash - - ./runtests.py --settings=path.to.django.settings - -The :setting:`DATABASES` setting in this test settings module needs to define +The :setting:`DATABASES` setting in any test settings module needs to define two databases: * A ``default`` database. This database should use the backend that diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 774de2a2fd..45f82b49e6 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -373,6 +373,7 @@ these changes. * The following private APIs will be removed: + - ``django.db.backend`` - ``django.db.close_connection()`` - ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()`` - ``django.db.transaction.is_managed()`` @@ -385,10 +386,15 @@ these changes. ``django.test.simple.DjangoTestSuiteRunner`` will be removed. Instead use ``django.test.runner.DiscoverRunner``. -* The module ``django.test._doctest`` and the classes - ``django.test.testcases.DocTestRunner`` and - ``django.test.testcases.OutputChecker`` will be removed. Instead use the - doctest module from the Python standard library. +* The module ``django.test._doctest`` will be removed. Instead use the doctest + module from the Python standard library. + +* The ``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting will be removed. + +* Usage of the hard-coded *Hold down "Control", or "Command" on a Mac, to select + more than one.* string to override or append to user-provided ``help_text`` in + forms for ManyToMany model fields will not be performed by Django anymore + either at the model or forms layer. 2.0 --- diff --git a/docs/internals/git.txt b/docs/internals/git.txt index 2b1a279d89..3904ff83d4 100644 --- a/docs/internals/git.txt +++ b/docs/internals/git.txt @@ -35,8 +35,9 @@ The Git repository includes several `branches`_: the next packaged release of Django. This is where most development activity is focused. -* ``stable/A.B.x`` are the maintenance branches. They are used to support - older versions of Django. +* ``stable/A.B.x`` are the branches where release preparation work happens. + They are also used for support and bugfix releases which occur as necessary + after the initial release of a major or minor version. * ``soc20XX/<project>`` branches were used by students who worked on Django during the 2009 and 2010 Google Summer of Code programs. @@ -83,13 +84,50 @@ coding style and how to generate and submit a patch. Other branches ============== -Django uses branches for two main purposes: +Django uses branches to prepare for releases of Django (whether they be +:term:`major <Major release>`, :term:`minor <Minor release>`, or +:term:`micro <Micro release>`). -1. Development of major or experimental features, to keep them from - affecting progress on other work in master. +In the past when Django was hosted on Subversion, branches were also used for +feature development. Now Django is hosted on Git and feature development is +done on contributor's forks, but the Subversion feature branches remain in Git +for historical reference. -2. Security and bugfix support for older releases of Django, during - their support lifetimes. +Stable branches +--------------- + +These branches can be found in the repository as ``stable/A.B.x`` +branches and will be created right after the first alpha is tagged. + +For example, immediately after *Django 1.5 alpha 1* was tagged, the branch +``stable/1.5.x`` was created and all further work on preparing the code for the +final 1.5 release was done there. + +These branches also provide limited bugfix support for the most recent released +version of Django and security support for the two most recently-released +versions of Django. + +For example, after the release of Django 1.5, the branch ``stable/1.5.x`` +receives only fixes for security and critical stability bugs, which are +eventually released as Django 1.5.1 and so on, ``stable/1.4.x`` receives only +security fixes, and ``stable/1.3.x`` no longer receives any updates. + +.. admonition:: Historical information + + This policy for handling ``stable/A.B.x`` branches was adopted starting + with the Django 1.5 release cycle. + + Previously, these branches weren't created until right after the releases + and the stabilization work occurred on the main repository branch. Thus, + no new features development work for the next release of Django could be + committed until the final release happened. + + For example, shortly after the release of Django 1.3 the branch + ``stable/1.3.x`` was created. Official support for that release has expired, + and so it no longer receives direct maintenance from the Django project. + However, that and all other similarly named branches continue to exist and + interested community members have occasionally used them to provide + unofficial support for old Django releases. Feature-development branches ---------------------------- @@ -203,30 +241,6 @@ All of the above-mentioned branches now reside in ``attic``. Finally, the repository contains ``soc2009/xxx`` and ``soc2010/xxx`` feature branches, used for Google Summer of Code projects. -Support and bugfix branches ---------------------------- - -In addition to fixing bugs in current master, the Django project provides -official bugfix support for the most recent released version of Django, and -security support for the two most recently-released versions of Django. - -This support is provided via branches in which the necessary bug or security -fixes are applied; the branches are then used as the basis for issuing bugfix -or security releases. - -These branches can be found in the repository as ``stable/A.B.x`` -branches, and new branches will be created there after each new Django -release. - -For example, shortly after the release of Django 1.0, the branch -``stable/1.0.x`` was created to receive bug fixes, and shortly after the -release of Django 1.1 the branch ``stable/1.1.x`` was created. - -Official support for the above mentioned releases has expired, and so they no -longer receive direct maintenance from the Django project. However, the -branches continue to exist and interested community members have occasionally -used them to provide unofficial support for old Django releases. - Tags ==== diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index fd985ddafc..5bda2e8add 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -183,13 +183,46 @@ OK, this is the fun part, where we actually push out a release! $ md5sum dist/Django-* $ sha1sum dist/Django-* - *FIXME: perhaps we should switch to sha256?* - #. Create a "checksums" file containing the hashes and release information. - You can start with `a previous checksums file`__ and replace the - dates, keys, links, and checksums. *FIXME: make a template file.* + Start with this template and insert the correct version, date, release URL + and checksums:: - __ https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt + This file contains MD5 and SHA1 checksums for the source-code tarball + of Django <<VERSION>>, released <<DATE>>. + + To use this file, you will need a working install of PGP or other + compatible public-key encryption software. You will also need to have + the Django release manager's public key in your keyring; this key has + the ID ``0x3684C0C08C8B2AE1`` and can be imported from the MIT + keyserver. For example, if using the open-source GNU Privacy Guard + implementation of PGP:: + + gpg --keyserver pgp.mit.edu --recv-key 0x3684C0C08C8B2AE1 + + Once the key is imported, verify this file:: + + gpg --verify <<THIS FILENAME>> + + Once you have verified this file, you can use normal MD5 and SHA1 + checksumming applications to generate the checksums of the Django + package and compare them to the checksums listed below. + + + Release package: + ================ + + Django <<VERSION>>: https://www.djangoproject.com/m/releases/<<URL>> + + + MD5 checksum: + ============= + + MD5(<<RELEASE TAR.GZ FILENAME>>)= <<MD5SUM>> + + SHA1 checksum: + ============== + + SHA1(<<RELEASE TAR.GZ FILENAME>>)= <<SHA1SUM>> #. Sign the checksum file (``gpg --clearsign Django-<version>.checksum.txt``). This generates a signed document, @@ -268,8 +301,7 @@ Now you're ready to actually put the release out there. To do this: of the docs by flipping the ``is_default`` flag to ``True`` on the appropriate ``DocumentRelease`` object in the ``docs.djangoproject.com`` database (this will automatically flip it to ``False`` for all - others). *FIXME: I had to do this via fab managepy:shell,docs but we should - probably make it possible to do via the admin.* + others); you can do this using the site's admin. #. Post the release announcement to the django-announce, django-developers and django-users mailing lists. This should @@ -289,7 +321,8 @@ You're almost done! All that's left to do now is: ``stable/1.?.x`` git branch), you'll want to create a new ``DocumentRelease`` object in the ``docs.djangoproject.com`` database for the new version's docs, and update the ``docs/fixtures/doc_releases.json`` - JSON fixture. *FIXME: what is the purpose of maintaining this fixture?* + JSON fixture, so people without access to the production DB can still + run an up-to-date copy of the docs site. #. Add the release in `Trac's versions list`_ if necessary. Not all versions are declared; take example on previous releases. diff --git a/docs/internals/release-process.txt b/docs/internals/release-process.txt index 29ce3914b4..2003e79079 100644 --- a/docs/internals/release-process.txt +++ b/docs/internals/release-process.txt @@ -39,49 +39,45 @@ issued from those branches. For more information about how the Django project issues new releases for security purposes, please see :doc:`our security policies <security>`. -Major releases --------------- +.. glossary:: -Major releases (1.0, 2.0, etc.) will happen very infrequently (think "years", -not "months"), and may represent major, sweeping changes to Django. + Major release + Major releases (1.0, 2.0, etc.) will happen very infrequently (think "years", + not "months"), and may represent major, sweeping changes to Django. -Minor releases --------------- + Minor release + Minor release (1.5, 1.6, etc.) will happen roughly every nine months -- see + `release process`_, below for details. These releases will contain new + features, improvements to existing features, and such. -Minor release (1.5, 1.6, etc.) will happen roughly every nine months -- see -`release process`_, below for details. These releases will contain new -features, improvements to existing features, and such. + .. _internal-release-deprecation-policy: -.. _internal-release-deprecation-policy: + A minor release may deprecate certain features from previous releases. If a + feature is deprecated in version ``A.B``, it will continue to work in versions + ``A.B`` and ``A.B+1`` but raise warnings. It will be removed in version + ``A.B+2``. -A minor release may deprecate certain features from previous releases. If a -feature is deprecated in version ``A.B``, it will continue to work in versions -``A.B`` and ``A.B+1`` but raise warnings. It will be removed in version -``A.B+2``. + So, for example, if we decided to start the deprecation of a function in + Django 1.5: -So, for example, if we decided to start the deprecation of a function in -Django 1.5: + * Django 1.5 will contain a backwards-compatible replica of the function which + will raise a ``PendingDeprecationWarning``. This warning is silent by + default; you can turn on display of these warnings with the ``-Wd`` option + of Python. -* Django 1.5 will contain a backwards-compatible replica of the function which - will raise a ``PendingDeprecationWarning``. This warning is silent by - default; you can turn on display of these warnings with the ``-Wd`` option - of Python. + * Django 1.6 will contain the backwards-compatible replica, but the warning + will be promoted to a full-fledged ``DeprecationWarning``. This warning is + *loud* by default, and will likely be quite annoying. -* Django 1.6 will contain the backwards-compatible replica, but the warning - will be promoted to a full-fledged ``DeprecationWarning``. This warning is - *loud* by default, and will likely be quite annoying. + * Django 1.7 will remove the feature outright. -* Django 1.7 will remove the feature outright. + Micro release + Micro releases (1.5.1, 1.6.2, 1.6.1, etc.) will be issued as needed, often to + fix security issues. -Micro releases --------------- - -Micro releases (1.5.1, 1.6.2, 1.6.1, etc.) will be issued as needed, often to -fix security issues. - -These releases will be 100% compatible with the associated minor release, unless -this is impossible for security reasons. So the answer to "should I upgrade to -the latest micro release?" will always be "yes." + These releases will be 100% compatible with the associated minor release, unless + this is impossible for security reasons. So the answer to "should I upgrade to + the latest micro release?" will always be "yes." .. _backwards-compatibility-policy: @@ -126,15 +122,15 @@ Django 1.6 and 1.7. At this point in time: * Features will be added to development master, to be released as Django 1.7. -* Critical bug fixes will be applied to the ``stable/1.6.X`` branch, and +* Critical bug fixes will be applied to the ``stable/1.6.x`` branch, and released as 1.6.1, 1.6.2, etc. -* Security fixes will be applied to ``master``, to the ``stable/1.6.X`` - branch, and to the ``stable/1.5.X`` branch. They will trigger the release of +* Security fixes will be applied to ``master``, to the ``stable/1.6.x`` + branch, and to the ``stable/1.5.x`` branch. They will trigger the release of ``1.6.1``, ``1.5.1``, etc. * Documentation fixes will be applied to master, and, if easily backported, to - the ``1.6.X`` branch. Bugfixes may also be backported. + the ``1.6.x`` branch. Bugfixes may also be backported. .. _release-process: @@ -193,9 +189,9 @@ Phase two will culminate with an alpha release. At this point, the Phase three: bugfixes ~~~~~~~~~~~~~~~~~~~~~ -The last third of a release is spent fixing bugs -- no new features will be -accepted during this time. We'll try to release a beta release after one month -and a release candidate after two months. +The last third of a release cycle is spent fixing bugs -- no new features will +be accepted during this time. We'll try to release a beta release after one +month and a release candidate after two months. The release candidate marks the string freeze, and it happens at least two weeks before the final release. After this point, new translatable strings diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 8753817256..77838ffcaa 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -16,14 +16,18 @@ Design your model ================= Although you can use Django without a database, it comes with an -object-relational mapper in which you describe your database layout in Python +`object-relational mapper`_ in which you describe your database layout in Python code. +.. _object-relational mapper: http://en.wikipedia.org/wiki/Object-relational_mapping + The :doc:`data-model syntax </topics/db/models>` offers many rich ways of representing your models -- so far, it's been solving two years' worth of database-schema problems. Here's a quick example, which might be saved in the file ``mysite/news/models.py``:: + from django.db import models + class Reporter(models.Model): full_name = models.CharField(max_length=70) @@ -55,8 +59,9 @@ tables in your database for whichever tables don't already exist. Enjoy the free API ================== -With that, you've got a free, and rich, :doc:`Python API </topics/db/queries>` to -access your data. The API is created on the fly, no code generation necessary: +With that, you've got a free, and rich, :doc:`Python API </topics/db/queries>` +to access your data. The API is created on the fly, no code generation +necessary: .. code-block:: python @@ -133,9 +138,9 @@ A dynamic admin interface: it's not just scaffolding -- it's the whole house ============================================================================ Once your models are defined, Django can automatically create a professional, -production ready :doc:`administrative interface </ref/contrib/admin/index>` -- a Web -site that lets authenticated users add, change and delete objects. It's as easy -as registering your model in the admin site:: +production ready :doc:`administrative interface </ref/contrib/admin/index>` -- +a Web site that lets authenticated users add, change and delete objects. It's +as easy as registering your model in the admin site:: # In models.py... @@ -171,9 +176,9 @@ application. Django encourages beautiful URL design and doesn't put any cruft in URLs, like ``.php`` or ``.asp``. To design URLs for an app, you create a Python module called a :doc:`URLconf -</topics/http/urls>`. A table of contents for your app, it contains a simple mapping -between URL patterns and Python callback functions. URLconfs also serve to -decouple URLs from Python code. +</topics/http/urls>`. A table of contents for your app, it contains a simple +mapping between URL patterns and Python callback functions. URLconfs also serve +to decouple URLs from Python code. Here's what a URLconf might look like for the ``Reporter``/``Article`` example above:: @@ -186,7 +191,7 @@ example above:: (r'^articles/(\d{4})/(\d{2})/(\d+)/$', 'news.views.article_detail'), ) -The code above maps URLs, as simple regular expressions, to the location of +The code above maps URLs, as simple `regular expressions`_, to the location of Python callback functions ("views"). The regular expressions use parenthesis to "capture" values from the URLs. When a user requests a page, Django runs through each pattern, in order, and stops at the first one that matches the @@ -194,6 +199,8 @@ requested URL. (If none of them matches, Django calls a special-case 404 view.) This is blazingly fast, because the regular expressions are compiled at load time. +.. _regular expressions: http://docs.python.org/2/howto/regex.html + Once one of the regexes matches, Django imports and calls the given view, which is a simple Python function. Each view gets passed a request object -- which contains request metadata -- and the values captured in the regex. @@ -214,6 +221,8 @@ Generally, a view retrieves data according to the parameters, loads a template and renders the template with the retrieved data. Here's an example view for ``year_archive`` from above:: + from django.shortcuts import render_to_response + def year_archive(request, year): a_list = Article.objects.filter(pub_date__year=year) return render_to_response('news/year_archive.html', {'year': year, 'article_list': a_list}) @@ -229,8 +238,8 @@ The code above loads the ``news/year_archive.html`` template. Django has a template search path, which allows you to minimize redundancy among templates. In your Django settings, you specify a list of directories to check -for templates. If a template doesn't exist in the first directory, it checks the -second, and so on. +for templates with :setting:`TEMPLATE_DIRS`. If a template doesn't exist in the +first directory, it checks the second, and so on. Let's say the ``news/year_archive.html`` template was found. Here's what that might look like: @@ -261,9 +270,10 @@ character). This is called a template filter, and it's a way to filter the value of a variable. In this case, the date filter formats a Python datetime object in the given format (as found in PHP's date function). -You can chain together as many filters as you'd like. You can write custom -filters. You can write custom template tags, which run custom Python code behind -the scenes. +You can chain together as many filters as you'd like. You can write :ref:`custom +template filters <howto-writing-custom-template-filters>`. You can write +:doc:`custom template tags </howto/custom-template-tags>`, which run custom +Python code behind the scenes. Finally, Django uses the concept of "template inheritance": That's what the ``{% extends "base.html" %}`` does. It means "First load the template called diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index d623bd8451..6e5988b15a 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -582,6 +582,8 @@ of this object. Let's fix that by editing the polls model (in the ``Choice``. On Python 3, simply replace ``__unicode__`` by ``__str__`` in the following example:: + from django.db import models + class Poll(models.Model): # ... def __unicode__(self): # Python 3: def __str__(self): diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 1987c51a67..dd3e86d8ae 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -158,6 +158,9 @@ you want when you register the object. Let's see how this works by re-ordering the fields on the edit form. Replace the ``admin.site.register(Poll)`` line with:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fields = ['pub_date', 'question'] @@ -179,6 +182,9 @@ of fields, choosing an intuitive order is an important usability detail. And speaking of forms with dozens of fields, you might want to split the form up into fieldsets:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question']}), @@ -198,6 +204,9 @@ You can assign arbitrary HTML classes to each fieldset. Django provides a This is useful when you have a long form that contains a number of fields that aren't commonly used:: + from django.contrib import admin + from polls.models import Poll + class PollAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question']}), @@ -218,6 +227,7 @@ Yet. There are two ways to solve this problem. The first is to register ``Choice`` with the admin just as we did with ``Poll``. That's easy:: + from django.contrib import admin from polls.models import Choice admin.site.register(Choice) @@ -342,6 +352,12 @@ representation of the output. You can improve that by giving that method (in :file:`polls/models.py`) a few attributes, as follows:: + import datetime + from django.utils import timezone + from django.db import models + + from polls.models import Poll + class Poll(models.Model): # ... def was_published_recently(self): diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 86cc5f97e6..120369172e 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -336,7 +336,7 @@ Put the following code in that template: <p>No polls are available.</p> {% endif %} -Now let's use that html template in our index view:: +Now let's update our ``index`` view in ``polls/views.py`` to use the template:: from django.http import HttpResponse from django.template import Context, loader @@ -393,6 +393,9 @@ Now, let's tackle the poll detail view -- the page that displays the question for a given poll. Here's the view:: from django.http import Http404 + from django.shortcuts import render + + from polls.models import Poll # ... def detail(request, poll_id): try: @@ -420,6 +423,8 @@ and raise :exc:`~django.http.Http404` if the object doesn't exist. Django provides a shortcut. Here's the ``detail()`` view, rewritten:: from django.shortcuts import render, get_object_or_404 + + from polls.models import Poll # ... def detail(request, poll_id): poll = get_object_or_404(Poll, pk=poll_id) diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 9f54243a3e..f81a7d6758 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -136,6 +136,8 @@ object. For more on :class:`~django.http.HttpRequest` objects, see the After somebody votes in a poll, the ``vote()`` view redirects to the results page for the poll. Let's write that view:: + from django.shortcuts import get_object_or_404, render + def results(request, poll_id): poll = get_object_or_404(Poll, pk=poll_id) return render(request, 'polls/results.html', {'poll': poll}) diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index a276763d67..39c3785f7c 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -503,8 +503,8 @@ of the process of creating polls. message: "No polls are available." and verifies the ``latest_poll_list`` is empty. Note that the :class:`django.test.TestCase` class provides some additional assertion methods. In these examples, we use -:meth:`~django.test.TestCase.assertContains()` and -:meth:`~django.test.TestCase.assertQuerysetEqual()`. +:meth:`~django.test.SimpleTestCase.assertContains()` and +:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`. In ``test_index_view_with_a_past_poll``, we create a poll and verify that it appears in the list. diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index ee0bf0f225..319bd4ebfe 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -55,7 +55,7 @@ View Default:: - ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] **Methods** @@ -114,7 +114,6 @@ TemplateView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.base.TemplateView` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.base.View` @@ -208,7 +207,7 @@ RedirectView urlpatterns = patterns('', - url(r'r^(?P<pk>\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'), + url(r'^(?P<pk>\d+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'), url(r'^go-to-django/$', RedirectView.as_view(url='http://djangoproject.com'), name='go-to-django'), ) diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 4dcb788779..1ebee254b1 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -33,7 +33,6 @@ ArchiveIndexView **Ancestors (MRO)** - * :class:`django.views.generic.dates.ArchiveIndexView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseArchiveIndexView` @@ -100,7 +99,6 @@ YearArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.YearArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseYearArchiveView` @@ -216,7 +214,6 @@ MonthArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.MonthArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseMonthArchiveView` @@ -317,7 +314,6 @@ WeekArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.WeekArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseWeekArchiveView` @@ -422,7 +418,6 @@ DayArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.DayArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseDayArchiveView` @@ -526,7 +521,6 @@ TodayArchiveView **Ancestors (MRO)** - * :class:`django.views.generic.dates.TodayArchiveView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseTodayArchiveView` @@ -585,7 +579,6 @@ DateDetailView **Ancestors (MRO)** - * :class:`django.views.generic.dates.DateDetailView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.dates.BaseDateDetailView` diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index b827c0005c..c133134d65 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -77,7 +77,6 @@ ListView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.list.ListView` * :class:`django.views.generic.list.MultipleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * :class:`django.views.generic.list.BaseListView` diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt index 555ba40cfb..c1fb2dcca9 100644 --- a/docs/ref/class-based-views/generic-editing.txt +++ b/docs/ref/class-based-views/generic-editing.txt @@ -36,7 +36,6 @@ FormView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.FormView` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseFormView`` * :class:`django.views.generic.edit.FormMixin` @@ -83,7 +82,6 @@ CreateView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.CreateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseCreateView`` @@ -126,7 +124,6 @@ UpdateView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.UpdateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseUpdateView`` @@ -169,7 +166,6 @@ DeleteView This view inherits methods and attributes from the following views: - * :class:`django.views.generic.edit.DeleteView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` * ``django.views.generic.edit.BaseDeleteView`` diff --git a/docs/ref/class-based-views/index.txt b/docs/ref/class-based-views/index.txt index a027953416..821edc0874 100644 --- a/docs/ref/class-based-views/index.txt +++ b/docs/ref/class-based-views/index.txt @@ -32,7 +32,7 @@ A class-based view is deployed into a URL pattern using the .. admonition:: Thread safety with view arguments Arguments passed to a view are shared between every instance of a view. - This means that you shoudn't use a list, dictionary, or any other + This means that you shouldn't use a list, dictionary, or any other mutable object as an argument to a view. If you do and the shared object is modified, the actions of one user visiting your view could have an effect on subsequent users visiting the same view. diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index 51d8628818..48d363b3b2 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -125,7 +125,7 @@ ModelFormMixin This is a required attribute if you are generating the form class automatically (e.g. using ``model``). Omitting this attribute will - result in all fields being used, but this behaviour is deprecated + result in all fields being used, but this behavior is deprecated and will be removed in Django 1.8. .. attribute:: success_url diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index 6796675529..377c85cc3b 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -60,6 +60,17 @@ TemplateResponseMixin altered later (e.g. in :ref:`template response middleware <template-response-middleware>`). + .. admonition:: Context processors + + ``TemplateResponse`` uses :class:`~django.template.RequestContext` + which means that callables defined in + :setting:`TEMPLATE_CONTEXT_PROCESSORS` may overwrite template + variables defined in your views. For example, if you subclass + :class:`DetailView <django.views.generic.detail.DetailView>` and + set ``context_object_name`` to ``user``, the + ``django.contrib.auth.context_processors.auth`` context processor + will happily overwrite your variable with current user. + If you need custom template loading or custom context object instantiation, create a ``TemplateResponse`` subclass and assign it to ``response_class``. diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index 394d078e5b..6deb7bdbf8 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -30,8 +30,8 @@ the following: * Install the docutils Python module (http://docutils.sf.net/). * **Optional:** Linking to templates requires the :setting:`ADMIN_FOR` setting to be configured. -* **Optional:** Using the admindocs bookmarklets requires the - :mod:`XViewMiddleware<django.middleware.doc>` to be installed. +* **Optional:** Using the admindocs bookmarklets requires + ``django.contrib.admindocs.middleware.XViewMiddleware`` to be installed. Once those steps are complete, you can start browsing the documentation by going to your admin interface and clicking the "Documentation" link in the @@ -156,7 +156,6 @@ Edit this object Using these bookmarklets requires that you are either logged into the :mod:`Django admin <django.contrib.admin>` as a :class:`~django.contrib.auth.models.User` with -:attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or -that the :mod:`django.middleware.doc` middleware and -:mod:`XViewMiddleware <django.middleware.doc>` are installed and you -are accessing the site from an IP address listed in :setting:`INTERNAL_IPS`. +:attr:`~django.contrib.auth.models.User.is_staff` set to `True`, or that the +``XViewMiddleware`` is installed and you are accessing the site from an IP +address listed in :setting:`INTERNAL_IPS`. diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 67e498ee91..7377f11a63 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -108,6 +108,8 @@ The ``ModelAdmin`` is very flexible. It has several options for dealing with customizing the interface. All options are defined on the ``ModelAdmin`` subclass:: + from django.contrib import admin + class AuthorAdmin(admin.ModelAdmin): date_hierarchy = 'pub_date' @@ -157,6 +159,8 @@ subclass:: For example, let's consider the following model:: + from django.db import models + class Author(models.Model): name = models.CharField(max_length=100) title = models.CharField(max_length=3) @@ -166,6 +170,8 @@ subclass:: and ``title`` fields, you would specify ``fields`` or ``exclude`` like this:: + from django.contrib import admin + class AuthorAdmin(admin.ModelAdmin): fields = ('name', 'title') @@ -234,6 +240,8 @@ subclass:: A full example, taken from the :class:`django.contrib.flatpages.models.FlatPage` model:: + from django.contrib import admin + class FlatPageAdmin(admin.ModelAdmin): fieldsets = ( (None, { @@ -356,6 +364,10 @@ subclass:: If your ``ModelForm`` and ``ModelAdmin`` both define an ``exclude`` option then ``ModelAdmin`` takes precedence:: + from django import forms + from django.contrib import admin + from myapp.models import Person + class PersonForm(forms.ModelForm): class Meta: @@ -452,13 +464,16 @@ subclass:: list_display = ('upper_case_name',) def upper_case_name(self, obj): - return ("%s %s" % (obj.first_name, obj.last_name)).upper() + return ("%s %s" % (obj.first_name, obj.last_name)).upper() upper_case_name.short_description = 'Name' * A string representing an attribute on the model. This behaves almost the same as the callable, but ``self`` in this context is the model instance. Here's a full model example:: + from django.db import models + from django.contrib import admin + class Person(models.Model): name = models.CharField(max_length=50) birthday = models.DateField() @@ -494,6 +509,8 @@ subclass:: Here's a full example model:: + from django.db import models + from django.contrib import admin from django.utils.html import format_html class Person(models.Model): @@ -519,6 +536,9 @@ subclass:: Here's a full example model:: + from django.db import models + from django.contrib import admin + class Person(models.Model): first_name = models.CharField(max_length=50) birthday = models.DateField() @@ -547,6 +567,8 @@ subclass:: For example:: + from django.db import models + from django.contrib import admin from django.utils.html import format_html class Person(models.Model): @@ -567,6 +589,27 @@ subclass:: The above will tell Django to order by the ``first_name`` field when trying to sort by ``colored_first_name`` in the admin. + * Elements of ``list_display`` can also be properties. Please note however, + that due to the way properties work in Python, setting + ``short_description`` on a property is only possible when using the + ``property()`` function and **not** with the ``@property`` decorator. + + For example:: + + class Person(object): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + + def my_property(self): + return self.first_name + ' ' + self.last_name + my_property.short_description = "Full name of the person" + + full_name = property(my_property) + + class PersonAdmin(admin.ModelAdmin): + list_display = ('full_name',) + + * .. versionadded:: 1.6 The field names in ``list_display`` will also appear as CSS classes in @@ -634,13 +677,13 @@ subclass:: ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``, ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``, for example:: - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): list_filter = ('is_staff', 'company') Field names in ``list_filter`` can also span relations using the ``__`` lookup, for example:: - class PersonAdmin(UserAdmin): + class PersonAdmin(admin.UserAdmin): list_filter = ('company__name',) * a class inheriting from ``django.contrib.admin.SimpleListFilter``, @@ -650,10 +693,10 @@ subclass:: from datetime import date + from django.contrib import admin from django.utils.translation import ugettext_lazy as _ - from django.contrib.admin import SimpleListFilter - class DecadeBornListFilter(SimpleListFilter): + class DecadeBornListFilter(admin.SimpleListFilter): # Human-readable title which will be displayed in the # right admin sidebar just above the filter options. title = _('decade born') @@ -689,7 +732,7 @@ subclass:: return queryset.filter(birthday__gte=date(1990, 1, 1), birthday__lte=date(1999, 12, 31)) - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): list_filter = (DecadeBornListFilter,) .. note:: @@ -732,11 +775,9 @@ subclass:: element is a class inheriting from ``django.contrib.admin.FieldListFilter``, for example:: - from django.contrib.admin import BooleanFieldListFilter - - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): list_filter = ( - ('is_staff', BooleanFieldListFilter), + ('is_staff', admin.BooleanFieldListFilter), ) .. note:: @@ -746,7 +787,7 @@ subclass:: It is possible to specify a custom template for rendering a list filter:: - class FilterWithCustomTemplate(SimpleListFilter): + class FilterWithCustomTemplate(admin.SimpleListFilter): template = "custom_template.html" See the default template provided by django (``admin/filter.html``) for @@ -771,12 +812,24 @@ subclass:: the list of objects on the admin change list page. This can save you a bunch of database queries. - The value should be either ``True`` or ``False``. Default is ``False``. + .. versionchanged:: dev - Note that Django will use - :meth:`~django.db.models.query.QuerySet.select_related`, - regardless of this setting if one of the ``list_display`` fields is a - ``ForeignKey``. + The value should be either a boolean, a list or a tuple. Default is + ``False``. + + When value is ``True``, ``select_related()`` will always be called. When + value is set to ``False``, Django will look at ``list_display`` and call + ``select_related()`` if any ``ForeignKey`` is present. + + If you need more fine-grained control, use a tuple (or list) as value for + ``list_select_related``. Empty tuple will prevent Django from calling + ``select_related`` at all. Any other tuple will be passed directly to + ``select_related`` as parameters. For example:: + + class ArticleAdmin(admin.ModelAdmin): + list_select_related = ('author', 'category') + + will call ``select_related('author', 'category')``. .. attribute:: ModelAdmin.ordering @@ -876,10 +929,11 @@ subclass:: the admin interface to provide feedback on the status of the objects being edited, for example:: + from django.contrib import admin from django.utils.html import format_html_join from django.utils.safestring import mark_safe - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): readonly_fields = ('address_report',) def address_report(self, instance): @@ -984,6 +1038,10 @@ subclass:: Performs a full-text match. This is like the default search method but uses an index. Currently this is only available for MySQL. + If you need to customize search you can use + :meth:`ModelAdmin.get_search_results` to provide additional or alternate + search behavior. + Custom template options ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1038,6 +1096,8 @@ templates used by the :class:`ModelAdmin` views: For example to attach ``request.user`` to the object prior to saving:: + from django.contrib import admin + class ArticleAdmin(admin.ModelAdmin): def save_model(self, request, obj, form, change): obj.user = request.user @@ -1071,7 +1131,7 @@ templates used by the :class:`ModelAdmin` views: is expected to return a ``list`` or ``tuple`` for ordering similar to the :attr:`ordering` attribute. For example:: - class PersonAdmin(ModelAdmin): + class PersonAdmin(admin.ModelAdmin): def get_ordering(self, request): if request.user.is_superuser: @@ -1079,6 +1139,39 @@ templates used by the :class:`ModelAdmin` views: else: return ['name'] +.. method:: ModelAdmin.get_search_results(self, request, queryset, search_term) + + .. versionadded:: 1.6 + + The ``get_search_results`` method modifies the list of objects displayed in + to those that match the provided search term. It accepts the request, a + queryset that applies the current filters, and the user-provided search term. + It returns a tuple containing a queryset modified to implement the search, and + a boolean indicating if the results may contain duplicates. + + The default implementation searches the fields named in :attr:`ModelAdmin.search_fields`. + + This method may be overridden with your own custom search method. For + example, you might wish to search by an integer field, or use an external + tool such as Solr or Haystack. You must establish if the queryset changes + implemented by your search method may introduce duplicates into the results, + and return ``True`` in the second element of the return value. + + For example, to enable search by integer field, you could use:: + + class PersonAdmin(admin.ModelAdmin): + list_display = ('name', 'age') + search_fields = ('name',) + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super(PersonAdmin, self).get_search_results(request, queryset, search_term) + try: + search_term_as_int = int(search_term) + queryset |= self.model.objects.filter(age=search_term_as_int) + except: + pass + return queryset, use_distinct + .. method:: ModelAdmin.save_related(self, request, form, formsets, change) The ``save_related`` method is given the ``HttpRequest``, the parent @@ -1298,6 +1391,8 @@ templates used by the :class:`ModelAdmin` views: Returns a :class:`~django.forms.ModelForm` class for use in the ``Formset`` on the changelist page. To use a custom form, for example:: + from django import forms + class MyForm(forms.ModelForm): pass @@ -1539,6 +1634,8 @@ information. The admin interface has the ability to edit models on the same page as a parent model. These are called inlines. Suppose you have these two models:: + from django.db import models + class Author(models.Model): name = models.CharField(max_length=100) @@ -1549,6 +1646,8 @@ information. You can edit the books authored by an author on the author page. You add inlines to a model by specifying them in a ``ModelAdmin.inlines``:: + from django.contrib import admin + class BookInline(admin.TabularInline): model = Book @@ -1629,6 +1728,11 @@ The ``InlineModelAdmin`` class adds: The dynamic link will not appear if the number of currently displayed forms exceeds ``max_num``, or if the user does not have JavaScript enabled. + .. versionadded:: 1.6 + + :meth:`InlineModelAdmin.get_extra` also allows you to customize the number + of extra forms. + .. _ref-contrib-admin-inline-max-num: .. attribute:: InlineModelAdmin.max_num @@ -1637,6 +1741,11 @@ The ``InlineModelAdmin`` class adds: doesn't directly correlate to the number of objects, but can if the value is small enough. See :ref:`model-formsets-max-num` for more information. + .. versionadded:: 1.6 + + :meth:`InlineModelAdmin.get_max_num` also allows you to customize the + maximum number of extra forms. + .. attribute:: InlineModelAdmin.raw_id_fields By default, Django's admin uses a select-box interface (<select>) for @@ -1676,12 +1785,55 @@ The ``InlineModelAdmin`` class adds: Returns a ``BaseInlineFormSet`` class for use in admin add/change views. See the example for :class:`ModelAdmin.get_formsets`. +.. method:: InlineModelAdmin.get_extra(self, request, obj=None, **kwargs) + + .. versionadded:: 1.6 + + Returns the number of extra inline forms to use. By default, returns the + :attr:`InlineModelAdmin.extra` attribute. + + Override this method to programmatically determine the number of extra + inline forms. For example, this may be based on the model instance + (passed as the keyword argument ``obj``):: + + class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_extra(self, request, obj=None, **kwargs): + extra = 2 + if obj: + return extra - obj.binarytree_set.count() + return extra + +.. method:: InlineModelAdmin.get_max_num(self, request, obj=None, **kwargs) + + .. versionadded:: 1.6 + + Returns the maximum number of extra inline forms to use. By default, + returns the :attr:`InlineModelAdmin.max_num` attribute. + + Override this method to programmatically determine the maximum number of + inline forms. For example, this may be based on the model instance + (passed as the keyword argument ``obj``):: + + class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_max_num(self, request, obj=None, **kwargs): + max_num = 10 + if obj.parent: + return max_num - 5 + return max_num + + Working with a model with two or more foreign keys to the same parent model --------------------------------------------------------------------------- It is sometimes possible to have more than one foreign key to the same model. Take this model for instance:: + from django.db import models + class Friendship(models.Model): to_person = models.ForeignKey(Person, related_name="friends") from_person = models.ForeignKey(Person, related_name="from_friends") @@ -1690,6 +1842,9 @@ If you wanted to display an inline on the ``Person`` admin add/change pages you need to explicitly define the foreign key since it is unable to do so automatically:: + from django.contrib import admin + from myapp.models import Friendship + class FriendshipInline(admin.TabularInline): model = Friendship fk_name = "to_person" @@ -1712,6 +1867,8 @@ widgets with inlines. Suppose we have the following models:: + from django.db import models + class Person(models.Model): name = models.CharField(max_length=128) @@ -1722,6 +1879,8 @@ Suppose we have the following models:: If you want to display many-to-many relations using an inline, you can do so by defining an ``InlineModelAdmin`` object for the relationship:: + from django.contrib import admin + class MembershipInline(admin.TabularInline): model = Group.members.through @@ -1768,6 +1927,8 @@ However, we still want to be able to edit that information inline. Fortunately, this is easy to do with inline admin models. Suppose we have the following models:: + from django.db import models + class Person(models.Model): name = models.CharField(max_length=128) @@ -1816,6 +1977,8 @@ Using generic relations as an inline It is possible to use an inline with generically related objects. Let's say you have the following models:: + from django.db import models + class Image(models.Model): image = models.ImageField(upload_to="images") content_type = models.ForeignKey(ContentType) diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 4fa119bc70..de9c5dcbd6 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -303,6 +303,15 @@ model: :class:`~django.contrib.contenttypes.generic.GenericForeignKey` will look for. + .. attribute:: GenericForeignKey.for_concrete_model + + .. versionadded:: 1.6 + + If ``False``, the field will be able to reference proxy models. Default + is ``True``. This mirrors the ``for_concrete_model`` argument to + :meth:`~django.contrib.contenttypes.models.ContentTypeManager.get_for_model`. + + .. admonition:: Primary key type compatibility The "object_id" field doesn't have to be the same type as the @@ -329,7 +338,7 @@ model: .. admonition:: Serializing references to ``ContentType`` objects If you're serializing data (for example, when generating - :class:`~django.test.TestCase.fixtures`) from a model that implements + :class:`~django.test.TransactionTestCase.fixtures`) from a model that implements generic relations, you should probably be using a natural key to uniquely identify related :class:`~django.contrib.contenttypes.models.ContentType` objects. See :ref:`natural keys<topics-serialization-natural-keys>` and @@ -492,7 +501,7 @@ information. Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular layouts, respectively. -.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False) +.. function:: generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True) Returns a ``GenericInlineFormSet`` using :func:`~django.forms.models.modelformset_factory`. @@ -502,3 +511,9 @@ information. are similar to those documented in :func:`~django.forms.models.modelformset_factory` and :func:`~django.forms.models.inlineformset_factory`. + + .. versionadded:: 1.6 + + The ``for_concrete_model`` argument corresponds to the + :class:`~django.contrib.contenttypes.generic.GenericForeignKey.for_concrete_model` + argument on ``GenericForeignKey``. diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 968ef0b07b..f8b3cf2646 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -120,7 +120,7 @@ Acquiring the token is straightforward: var csrftoken = getCookie('csrftoken'); The above code could be simplified by using the `jQuery cookie plugin -<http://plugins.jquery.com/project/Cookie>`_ to replace ``getCookie``: +<http://plugins.jquery.com/cookie/>`_ to replace ``getCookie``: .. code-block:: javascript @@ -384,6 +384,7 @@ Utilities the middleware. Example:: from django.views.decorators.csrf import csrf_exempt + from django.http import HttpResponse @csrf_exempt def my_view(request): diff --git a/docs/ref/contrib/formtools/form-preview.txt b/docs/ref/contrib/formtools/form-preview.txt index 011e72c2e0..b86cc4dc90 100644 --- a/docs/ref/contrib/formtools/form-preview.txt +++ b/docs/ref/contrib/formtools/form-preview.txt @@ -53,6 +53,7 @@ How to use ``FormPreview`` overrides the ``done()`` method:: from django.contrib.formtools.preview import FormPreview + from django.http import HttpResponseRedirect from myapp.models import SomeModel class SomeModelFormPreview(FormPreview): diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index f85ae8356d..7795a32c09 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -420,8 +420,10 @@ Advanced ``WizardView`` methods .. method:: WizardView.get_form(step=None, data=None, files=None) This method constructs the form for a given ``step``. If no ``step`` is - defined, the current step will be determined automatically. - The method gets three arguments: + defined, the current step will be determined automatically. If you override + ``get_form``, however, you will need to set ``step`` yourself using + ``self.steps.current`` as in the example below. The method gets three + arguments: * ``step`` -- The step for which the form instance should be generated. * ``data`` -- Gets passed to the form's data argument @@ -433,6 +435,11 @@ Advanced ``WizardView`` methods def get_form(self, step=None, data=None, files=None): form = super(MyWizard, self).get_form(step, data, files) + + # determine the step if not given + if step is None: + step = self.steps.current + if step == '1': form.user = self.request.user return form diff --git a/docs/ref/contrib/gis/geoip.txt b/docs/ref/contrib/gis/geoip.txt index 2444849a19..b6aca6b211 100644 --- a/docs/ref/contrib/gis/geoip.txt +++ b/docs/ref/contrib/gis/geoip.txt @@ -8,8 +8,7 @@ Geolocation with GeoIP :synopsis: High-level Python interface for MaxMind's GeoIP C library. The :class:`GeoIP` object is a ctypes wrapper for the -`MaxMind GeoIP C API`__. [#]_ This interface is a BSD-licensed alternative -to the GPL-licensed `Python GeoIP`__ interface provided by MaxMind. +`MaxMind GeoIP C API`__. [#]_ In order to perform IP-based geolocation, the :class:`GeoIP` object requires the GeoIP C libary and either the GeoIP `Country`__ or `City`__ @@ -20,7 +19,6 @@ you set :setting:`GEOIP_PATH` with in your settings. See the example and reference below for more details. __ http://www.maxmind.com/app/c -__ http://www.maxmind.com/app/python __ http://www.maxmind.com/app/country __ http://www.maxmind.com/app/city __ http://www.maxmind.com/download/geoip/database/ diff --git a/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh index 081b5f2656..67c82a8b25 100755 --- a/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh +++ b/docs/ref/contrib/gis/install/create_template_postgis-1.5.sh @@ -1,9 +1,15 @@ #!/usr/bin/env bash -POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 +if [[ `uname -r | grep el6` ]]; then + POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis + POSTGIS_SQL_FILE=$POSTGIS_SQL_PATH/postgis-64.sql +else + POSTGIS_SQL_PATH=`pg_config --sharedir`/contrib/postgis-1.5 + POSTGIS_SQL_FILE=$POSTGIS_SQL_PATH/postgis.sql +fi createdb -E UTF8 template_postgis # Create the template spatial database. createlang -d template_postgis plpgsql # Adding PLPGSQL language support. psql -d postgres -c "UPDATE pg_database SET datistemplate='true' WHERE datname='template_postgis';" -psql -d template_postgis -f $POSTGIS_SQL_PATH/postgis.sql # Loading the PostGIS SQL routines +psql -d template_postgis -f $POSTGIS_SQL_FILE # Loading the PostGIS SQL routines psql -d template_postgis -f $POSTGIS_SQL_PATH/spatial_ref_sys.sql psql -d template_postgis -c "GRANT ALL ON geometry_columns TO PUBLIC;" # Enabling users to alter spatial tables. psql -d template_postgis -c "GRANT ALL ON geography_columns TO PUBLIC;" diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt index 0a376bca18..608c37bb7f 100644 --- a/docs/ref/contrib/messages.txt +++ b/docs/ref/contrib/messages.txt @@ -373,4 +373,3 @@ behavior: * :setting:`MESSAGE_LEVEL` * :setting:`MESSAGE_STORAGE` * :setting:`MESSAGE_TAGS` -* :ref:`SESSION_COOKIE_DOMAIN<messages-session_cookie_domain>` diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index d37ee83378..56a15cb9e0 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -280,6 +280,46 @@ Here's an example of a :doc:`URLconf </topics/http/urls>` using both:: .. _URLconf: ../url_dispatch/ +Sitemap for static views +======================== + +Often you want the search engine crawlers to index views which are neither +object detail pages nor flatpages. The solution is to explicitly list URL +names for these views in ``items`` and call +:func:`~django.core.urlresolvers.reverse` in the ``location`` method of +the sitemap. For example:: + + # sitemaps.py + from django.contrib import sitemaps + from django.core.urlresolvers import reverse + + class StaticViewSitemap(sitemaps.Sitemap): + priority = 0.5 + changefreq = 'daily' + + def items(self): + return ['main', 'about', 'license'] + + def location(self, item): + return reverse(item) + + # urls.py + from django.conf.urls import patterns, url + from .sitemaps import StaticViewSitemap + + sitemaps = { + 'static': StaticViewSitemap, + } + + urlpatterns = patterns('', + url(r'^$', 'views.main', name='main'), + url(r'^about/$', 'views.about', name='about'), + url(r'^license/$', 'views.license', name='license'), + # ... + url(r'^sitemap\.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps}) + ) + + Creating a sitemap index ======================== diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 02159c415b..51d038d187 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -137,25 +137,29 @@ into those elements. See `a complex example`_ below that uses a description template. - There is also a way to pass additional information to title and description - templates, if you need to supply more than the two variables mentioned - before. You can provide your implementation of ``get_context_data`` method - in your Feed subclass. For example:: + .. method:: Feed.get_context_data(self, **kwargs) - from mysite.models import Article - from django.contrib.syndication.views import Feed + .. versionadded:: 1.6 - class ArticlesFeed(Feed): - title = "My articles" - description_template = "feeds/articles.html" + There is also a way to pass additional information to title and description + templates, if you need to supply more than the two variables mentioned + before. You can provide your implementation of ``get_context_data`` method + in your ``Feed`` subclass. For example:: - def items(self): - return Article.objects.order_by('-pub_date')[:5] + from mysite.models import Article + from django.contrib.syndication.views import Feed - def get_context_data(self, **kwargs): - context = super(ArticlesFeed, self).get_context_data(**kwargs) - context['foo'] = 'bar' - return context + class ArticlesFeed(Feed): + title = "My articles" + description_template = "feeds/articles.html" + + def items(self): + return Article.objects.order_by('-pub_date')[:5] + + def get_context_data(self, **kwargs): + context = super(ArticlesFeed, self).get_context_data(**kwargs) + context['foo'] = 'bar' + return context And the template: diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 7555acaaba..a648ac1709 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -135,16 +135,17 @@ configuration in :setting:`DATABASES`:: Since Django 1.6, autocommit is turned on by default. This configuration is ignored and can be safely removed. +.. _database-isolation-level: + Isolation level --------------- .. versionadded:: 1.6 Like PostgreSQL itself, Django defaults to the ``READ COMMITTED`` `isolation -level <postgresql-isolation-levels>`_. If you need a higher isolation level -such as ``REPEATABLE READ`` or ``SERIALIZABLE``, set it in the -:setting:`OPTIONS` part of your database configuration in -:setting:`DATABASES`:: +level`_. If you need a higher isolation level such as ``REPEATABLE READ`` or +``SERIALIZABLE``, set it in the :setting:`OPTIONS` part of your database +configuration in :setting:`DATABASES`:: import psycopg2.extensions @@ -161,7 +162,7 @@ such as ``REPEATABLE READ`` or ``SERIALIZABLE``, set it in the handle exceptions raised on serialization failures. This option is designed for advanced uses. -.. _postgresql-isolation-levels: http://www.postgresql.org/docs/current/static/transaction-iso.html +.. _isolation level: http://www.postgresql.org/docs/current/static/transaction-iso.html Indexes for ``varchar`` and ``text`` columns -------------------------------------------- @@ -803,5 +804,5 @@ the support channels provided by each 3rd party project. .. _IBM DB2: http://code.google.com/p/ibm-db/ .. _Microsoft SQL Server 2005: http://code.google.com/p/django-mssql/ .. _Firebird: http://code.google.com/p/django-firebird/ -.. _ODBC: http://code.google.com/p/django-pyodbc/ +.. _ODBC: https://github.com/aurorasoftware/django-pyodbc/ .. _ADSDB: http://code.google.com/p/adsdb-django/ diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 2f2880679c..e21e3d2766 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -227,6 +227,15 @@ a natural key definition. If you are dumping ``contrib.auth`` ``Permission`` objects or ``contrib.contenttypes`` ``ContentType`` objects, you should probably be using this flag. +.. versionadded:: 1.6 + +.. django-admin-option:: --pks + +By default, ``dumpdata`` will output all the records of the model, but +you can use the ``--pks`` option to specify a comma seperated list of +primary keys on which to filter. This is only available when dumping +one model. + flush ----- @@ -1314,6 +1323,8 @@ clearsessions .. django-admin:: clearsessions +.. versionadded:: 1.5 + Can be run as a cron job or directly to clean out expired sessions. ``django.contrib.sitemaps`` diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index f9a1715180..b15bbea8fa 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -44,9 +44,24 @@ SuspiciousOperation ------------------- .. exception:: SuspiciousOperation - The :exc:`SuspiciousOperation` exception is raised when a user has performed - an operation that should be considered suspicious from a security perspective, - such as tampering with a session cookie. + The :exc:`SuspiciousOperation` exception is raised when a user has + performed an operation that should be considered suspicious from a security + perspective, such as tampering with a session cookie. Subclasses of + SuspiciousOperation include: + + * DisallowedHost + * DisallowedModelAdminLookup + * DisallowedRedirect + * InvalidSessionKey + * SuspiciousFileOperation + * SuspiciousMultipartForm + * SuspiciousSession + * WizardViewCookieModified + + If a ``SuspiciousOperation`` exception reaches the WSGI handler level it is + logged at the ``Error`` level and results in + a :class:`~django.http.HttpResponseBadRequest`. See the :doc:`logging + documentation </topics/logging/>` for more information. PermissionDenied ---------------- @@ -137,10 +152,16 @@ The Django wrappers for database exceptions behave exactly the same as the underlying database exceptions. See :pep:`249`, the Python Database API Specification v2.0, for further information. +As per :pep:`3134`, a ``__cause__`` attribute is set with the original +(underlying) database exception, allowing access to any additional +information provided. (Note that this attribute is available under +both Python 2 and Python 3, although :pep:`3134` normally only applies +to Python 3.) + .. versionchanged:: 1.6 Previous version of Django only wrapped ``DatabaseError`` and - ``IntegrityError``. + ``IntegrityError``, and did not provide ``__cause__``. .. exception:: models.ProtectedError diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 34ed2e493e..67e3aab712 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -154,6 +154,7 @@ you include ``initial`` when instantiating the ``Form``, then the latter at the field level and at the form instance level, and the latter gets precedence:: + >>> from django import forms >>> class CommentForm(forms.Form): ... name = forms.CharField(initial='class') ... url = forms.URLField() @@ -238,6 +239,7 @@ When the ``Form`` is valid, ``cleaned_data`` will include a key and value for fields. In this example, the data dictionary doesn't include a value for the ``nick_name`` field, but ``cleaned_data`` includes it, with an empty value:: + >>> from django.forms import Form >>> class OptionalPersonForm(Form): ... first_name = CharField() ... last_name = CharField() @@ -327,54 +329,54 @@ a form object, and each rendering method returns a Unicode object. .. method:: Form.as_p - ``as_p()`` renders the form as a series of ``<p>`` tags, with each ``<p>`` - containing one field:: +``as_p()`` renders the form as a series of ``<p>`` tags, with each ``<p>`` +containing one field:: - >>> f = ContactForm() - >>> f.as_p() - u'<p><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></p>\n<p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p>\n<p><label for="id_sender">Sender:</label> <input type="text" name="sender" id="id_sender" /></p>\n<p><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></p>' - >>> print(f.as_p()) - <p><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></p> - <p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p> - <p><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" /></p> - <p><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></p> + >>> f = ContactForm() + >>> f.as_p() + u'<p><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></p>\n<p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p>\n<p><label for="id_sender">Sender:</label> <input type="text" name="sender" id="id_sender" /></p>\n<p><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></p>' + >>> print(f.as_p()) + <p><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></p> + <p><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></p> + <p><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" /></p> + <p><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></p> ``as_ul()`` ~~~~~~~~~~~ .. method:: Form.as_ul - ``as_ul()`` renders the form as a series of ``<li>`` tags, with each - ``<li>`` containing one field. It does *not* include the ``<ul>`` or - ``</ul>``, so that you can specify any HTML attributes on the ``<ul>`` for - flexibility:: +``as_ul()`` renders the form as a series of ``<li>`` tags, with each +``<li>`` containing one field. It does *not* include the ``<ul>`` or +``</ul>``, so that you can specify any HTML attributes on the ``<ul>`` for +flexibility:: - >>> f = ContactForm() - >>> f.as_ul() - u'<li><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></li>\n<li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li>\n<li><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" /></li>\n<li><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></li>' - >>> print(f.as_ul()) - <li><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></li> - <li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li> - <li><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" /></li> - <li><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></li> + >>> f = ContactForm() + >>> f.as_ul() + u'<li><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></li>\n<li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li>\n<li><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" /></li>\n<li><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></li>' + >>> print(f.as_ul()) + <li><label for="id_subject">Subject:</label> <input id="id_subject" type="text" name="subject" maxlength="100" /></li> + <li><label for="id_message">Message:</label> <input type="text" name="message" id="id_message" /></li> + <li><label for="id_sender">Sender:</label> <input type="email" name="sender" id="id_sender" /></li> + <li><label for="id_cc_myself">Cc myself:</label> <input type="checkbox" name="cc_myself" id="id_cc_myself" /></li> ``as_table()`` ~~~~~~~~~~~~~~ .. method:: Form.as_table - Finally, ``as_table()`` outputs the form as an HTML ``<table>``. This is - exactly the same as ``print``. In fact, when you ``print`` a form object, - it calls its ``as_table()`` method behind the scenes:: +Finally, ``as_table()`` outputs the form as an HTML ``<table>``. This is +exactly the same as ``print``. In fact, when you ``print`` a form object, +it calls its ``as_table()`` method behind the scenes:: - >>> f = ContactForm() - >>> f.as_table() - u'<tr><th><label for="id_subject">Subject:</label></th><td><input id="id_subject" type="text" name="subject" maxlength="100" /></td></tr>\n<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" /></td></tr>\n<tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" /></td></tr>\n<tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself" /></td></tr>' - >>> print(f.as_table()) - <tr><th><label for="id_subject">Subject:</label></th><td><input id="id_subject" type="text" name="subject" maxlength="100" /></td></tr> - <tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" /></td></tr> - <tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" /></td></tr> - <tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself" /></td></tr> + >>> f = ContactForm() + >>> f.as_table() + u'<tr><th><label for="id_subject">Subject:</label></th><td><input id="id_subject" type="text" name="subject" maxlength="100" /></td></tr>\n<tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" /></td></tr>\n<tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" /></td></tr>\n<tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself" /></td></tr>' + >>> print(f.as_table()) + <tr><th><label for="id_subject">Subject:</label></th><td><input id="id_subject" type="text" name="subject" maxlength="100" /></td></tr> + <tr><th><label for="id_message">Message:</label></th><td><input type="text" name="message" id="id_message" /></td></tr> + <tr><th><label for="id_sender">Sender:</label></th><td><input type="email" name="sender" id="id_sender" /></td></tr> + <tr><th><label for="id_cc_myself">Cc myself:</label></th><td><input type="checkbox" name="cc_myself" id="id_cc_myself" /></td></tr> Styling required or erroneous form rows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -391,6 +393,8 @@ attributes to required rows or to rows with errors: simply set the :attr:`Form.error_css_class` and/or :attr:`Form.required_css_class` attributes:: + from django.forms import Form + class ContactForm(Form): error_css_class = 'error' required_css_class = 'required' @@ -621,23 +625,23 @@ For a field's list of errors, access the field's ``errors`` attribute. .. attribute:: BoundField.errors - A list-like object that is displayed as an HTML ``<ul class="errorlist">`` - when printed:: +A list-like object that is displayed as an HTML ``<ul class="errorlist">`` +when printed:: - >>> data = {'subject': 'hi', 'message': '', 'sender': '', 'cc_myself': ''} - >>> f = ContactForm(data, auto_id=False) - >>> print(f['message']) - <input type="text" name="message" /> - >>> f['message'].errors - [u'This field is required.'] - >>> print(f['message'].errors) - <ul class="errorlist"><li>This field is required.</li></ul> - >>> f['subject'].errors - [] - >>> print(f['subject'].errors) + >>> data = {'subject': 'hi', 'message': '', 'sender': '', 'cc_myself': ''} + >>> f = ContactForm(data, auto_id=False) + >>> print(f['message']) + <input type="text" name="message" /> + >>> f['message'].errors + [u'This field is required.'] + >>> print(f['message'].errors) + <ul class="errorlist"><li>This field is required.</li></ul> + >>> f['subject'].errors + [] + >>> print(f['subject'].errors) - >>> str(f['subject'].errors) - '' + >>> str(f['subject'].errors) + '' .. method:: BoundField.label_tag(contents=None, attrs=None) @@ -779,6 +783,7 @@ example, ``BeatleForm`` subclasses both ``PersonForm`` and ``InstrumentForm`` (in that order), and its field list includes the fields from the parent classes:: + >>> from django.forms import Form >>> class PersonForm(Form): ... first_name = CharField() ... last_name = CharField() diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 8e1a4b34d1..69e3aa71ad 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -48,6 +48,7 @@ By default, each ``Field`` class assumes the value is required, so if you pass an empty value -- either ``None`` or the empty string (``""``) -- then ``clean()`` will raise a ``ValidationError`` exception:: + >>> from django import forms >>> f = forms.CharField() >>> f.clean('foo') u'foo' @@ -107,6 +108,7 @@ behavior doesn't result in an adequate label. Here's a full example ``Form`` that implements ``label`` for two of its fields. We've specified ``auto_id=False`` to simplify the output:: + >>> from django import forms >>> class CommentForm(forms.Form): ... name = forms.CharField(label='Your name') ... url = forms.URLField(label='Your Web site', required=False) @@ -130,6 +132,7 @@ To specify dynamic initial data, see the :attr:`Form.initial` parameter. The use-case for this is when you want to display an "empty" form in which a field is initialized to a particular value. For example:: + >>> from django import forms >>> class CommentForm(forms.Form): ... name = forms.CharField(initial='Your name') ... url = forms.URLField(initial='http://') @@ -205,6 +208,7 @@ methods (e.g., ``as_ul()``). Here's a full example ``Form`` that implements ``help_text`` for two of its fields. We've specified ``auto_id=False`` to simplify the output:: + >>> from django import forms >>> class HelpTextContactForm(forms.Form): ... subject = forms.CharField(max_length=100, help_text='100 characters max.') ... message = forms.CharField() @@ -236,6 +240,7 @@ The ``error_messages`` argument lets you override the default messages that the field will raise. Pass in a dictionary with keys matching the error messages you want to override. For example, here is the default error message:: + >>> from django import forms >>> generic = forms.CharField() >>> generic.clean('') Traceback (most recent call last): @@ -853,6 +858,7 @@ Slightly complex built-in ``Field`` classes The list of fields that should be used to validate the field's value (in the order in which they are provided). + >>> from django.forms import ComboField >>> f = ComboField(fields=[CharField(max_length=20), EmailField()]) >>> f.clean('test@example.com') u'test@example.com' @@ -1001,6 +1007,8 @@ objects (in the case of ``ModelMultipleChoiceField``) into the object, and should return a string suitable for representing it. For example:: + from django.forms import ModelChoiceField + class MyModelChoiceField(ModelChoiceField): def label_from_instance(self, obj): return "My Object #%i" % obj.id diff --git a/docs/ref/forms/models.txt b/docs/ref/forms/models.txt index 9b3480758a..b54056af0c 100644 --- a/docs/ref/forms/models.txt +++ b/docs/ref/forms/models.txt @@ -5,7 +5,7 @@ Model Form Functions .. module:: django.forms.models :synopsis: Django's functions for building model forms and formsets. -.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None) +.. function:: modelform_factory(model, form=ModelForm, fields=None, exclude=None, formfield_callback=None, widgets=None, localized_fields=None) Returns a :class:`~django.forms.ModelForm` class for the given ``model``. You can optionally pass a ``form`` argument to use as a starting point for @@ -20,6 +20,8 @@ Model Form Functions ``widgets`` is a dictionary of model field names mapped to a widget. + ``localized_fields`` is a list of names of fields which should be localized. + ``formfield_callback`` is a callable that takes a model field and returns a form field. @@ -31,14 +33,16 @@ Model Form Functions ``fields`` or ``exclude``, or the corresponding attributes on the form's inner ``Meta`` class. See :ref:`modelforms-selecting-fields` for more information. Omitting any definition of the fields to use will result in all - fields being used, but this behaviour is deprecated. + fields being used, but this behavior is deprecated. -.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False) + The ``localized_fields`` parameter was added. + +.. function:: modelformset_factory(model, form=ModelForm, formfield_callback=None, formset=BaseModelFormSet, extra=1, can_delete=False, can_order=False, max_num=None, fields=None, exclude=None, widgets=None, validate_max=False, localized_fields=None) Returns a ``FormSet`` class for the given ``model`` class. Arguments ``model``, ``form``, ``fields``, ``exclude``, - ``formfield_callback`` and ``widgets`` are all passed through to + ``formfield_callback``, ``widgets`` and ``localized_fields`` are all passed through to :func:`~django.forms.models.modelform_factory`. Arguments ``formset``, ``extra``, ``max_num``, ``can_order``, @@ -50,9 +54,9 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets`` and the ``validate_max`` parameters were added. + The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. -.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False) +.. function:: inlineformset_factory(parent_model, model, form=ModelForm, formset=BaseInlineFormSet, fk_name=None, fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, widgets=None, validate_max=False, localized_fields=None) Returns an ``InlineFormSet`` using :func:`modelformset_factory` with defaults of ``formset=BaseInlineFormSet``, ``can_delete=True``, and @@ -65,4 +69,4 @@ Model Form Functions .. versionchanged:: 1.6 - The ``widgets`` and the ``validate_max`` parameters were added. + The ``widgets``, ``validate_max`` and ``localized_fields`` parameters were added. diff --git a/docs/ref/forms/validation.txt b/docs/ref/forms/validation.txt index 3aaa69b6ea..87c9764f64 100644 --- a/docs/ref/forms/validation.txt +++ b/docs/ref/forms/validation.txt @@ -183,6 +183,9 @@ the ``default_validators`` attribute. Simple validators can be used to validate values inside the field, let's have a look at Django's ``SlugField``:: + from django.forms import CharField + from django.core import validators + class SlugField(CharField): default_validators = [validators.validate_slug] @@ -252,6 +255,8 @@ we want to make sure that the ``recipients`` field always contains the address don't want to put it into the general ``MultiEmailField`` class. Instead, we write a cleaning method that operates on the ``recipients`` field, like so:: + from django import forms + class ContactForm(forms.Form): # Everything as before. ... @@ -289,6 +294,8 @@ common method is to display the error at the top of the form. To create such an error, you can raise a ``ValidationError`` from the ``clean()`` method. For example:: + from django import forms + class ContactForm(forms.Form): # Everything as before. ... @@ -321,6 +328,8 @@ here and leaving it up to you and your designers to work out what works effectively in your particular situation. Our new code (replacing the previous sample) looks like this:: + from django import forms + class ContactForm(forms.Form): # Everything as before. ... diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 678f2e6949..0f6917d44c 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -201,6 +201,7 @@ foundation for custom widgets. .. code-block:: python + >>> from django import forms >>> name = forms.TextInput(attrs={'size': 10, 'title': 'Your name',}) >>> name.render('name', 'A name') u'<input title="Your name" type="text" name="name" value="A name" size="10" />' @@ -249,6 +250,8 @@ foundation for custom widgets. :class:`~datetime.datetime` value into a list with date and time split into two separate values:: + from django.forms import MultiWidget + class SplitDateTimeWidget(MultiWidget): # ... diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 03885a2215..4898bab636 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -71,19 +71,6 @@ Adds a few conveniences for perfectionists: * Sends broken link notification emails to :setting:`MANAGERS` (see :doc:`/howto/error-reporting`). -View metadata middleware ------------------------- - -.. module:: django.middleware.doc - :synopsis: Middleware to help your app self-document. - -.. class:: XViewMiddleware - -Sends custom ``X-View`` HTTP headers to HEAD requests that come from IP -addresses defined in the :setting:`INTERNAL_IPS` setting. This is used by -Django's :doc:`automatic documentation system </ref/contrib/admin/admindocs>`. -Depends on :class:`~django.contrib.auth.middleware.AuthenticationMiddleware`. - GZip middleware --------------- diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 99ba78cb09..8146dfd341 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -80,9 +80,10 @@ If a field has ``blank=False``, the field will be required. .. attribute:: Field.choices -An iterable (e.g., a list or tuple) of 2-tuples to use as choices for this -field. If this is given, the default form widget will be a select box with -these choices instead of the standard text field. +An iterable (e.g., a list or tuple) consisting itself of iterables of exactly +two items (e.g. ``[(A, B), (A, B) ...]``) to use as choices for this field. If +this is given, the default form widget will be a select box with these choices +instead of the standard text field. The first element in each tuple is the actual value to be stored, and the second element is the human-readable name. For example:: @@ -97,6 +98,8 @@ second element is the human-readable name. For example:: Generally, it's best to define choices inside a model class, and to define a suitably-named constant for each value:: + from django.db import models + class Student(models.Model): FRESHMAN = 'FR' SOPHOMORE = 'SO' @@ -290,7 +293,12 @@ records with the same ``title`` and ``pub_date``. Note that if you set this to point to a :class:`DateTimeField`, only the date portion of the field will be considered. -This is enforced by model validation but not at the database level. +This is enforced by :meth:`Model.validate_unique()` during model validation +but not at the database level. If any :attr:`~Field.unique_for_date` constraint +involves fields that are not part of a :class:`~django.forms.ModelForm` (for +example, if one of the fields is listed in ``exclude`` or has +:attr:`editable=False<Field.editable>`), :meth:`Model.validate_unique()` will +skip validation for that particular constraint. ``unique_for_month`` -------------------- @@ -365,7 +373,7 @@ to filter a queryset on a ``BinaryField`` value. Although you might think about storing files in the database, consider that it is bad design in 99% of the cases. This field is *not* a replacement for - proper :doc`static files </howto/static-files>` handling. + proper :doc:`static files </howto/static-files/index>` handling. ``BooleanField`` ---------------- @@ -889,7 +897,8 @@ The value ``0`` is accepted for backward compatibility reasons. .. class:: PositiveSmallIntegerField([**options]) Like a :class:`PositiveIntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values up to 32767 are safe in all databases +supported by Django. ``SlugField`` ------------- @@ -917,7 +926,8 @@ of some other value. You can do this automatically in the admin using .. class:: SmallIntegerField([**options]) Like an :class:`IntegerField`, but only allows values under a certain -(database-dependent) point. +(database-dependent) point. Values from -32768 to 32767 are safe in all databases +supported by Django. ``TextField`` ------------- @@ -994,12 +1004,15 @@ relationship with itself -- use ``models.ForeignKey('self')``. If you need to create a relationship on a model that has not yet been defined, you can use the name of the model, rather than the model object itself:: + from django.db import models + class Car(models.Model): manufacturer = models.ForeignKey('Manufacturer') # ... class Manufacturer(models.Model): # ... + pass To refer to models defined in another application, you can explicitly specify a model with the full application label. For example, if the ``Manufacturer`` @@ -1132,6 +1145,9 @@ The possible values for :attr:`~ForeignKey.on_delete` are found in necessary to avoid executing queries at the time your models.py is imported:: + from django.db import models + from django.contrib.auth.models import User + def get_sentinel_user(): return User.objects.get_or_create(username='deleted')[0] @@ -1204,6 +1220,8 @@ that control how the relationship functions. Only used in the definition of ManyToManyFields on self. Consider the following model:: + from django.db import models + class Person(models.Model): friends = models.ManyToManyField("self") diff --git a/docs/ref/models/instances.txt b/docs/ref/models/instances.txt index b4b162a9ea..f989ff1bec 100644 --- a/docs/ref/models/instances.txt +++ b/docs/ref/models/instances.txt @@ -34,6 +34,8 @@ that, you need to :meth:`~Model.save()`. 1. Add a classmethod on the model class:: + from django.db import models + class Book(models.Model): title = models.CharField(max_length=100) @@ -105,6 +107,7 @@ individually. You'll need to call ``full_clean`` manually when you want to run one-step model validation for your own manually created models. For example:: + from django.core.exceptions import ValidationError try: article.full_clean() except ValidationError as e: @@ -132,6 +135,7 @@ automatically provide a value for a field, or to do validation that requires access to more than a single field:: def clean(self): + import datetime from django.core.exceptions import ValidationError # Don't allow draft entries to have a pub_date. if self.status == 'draft' and self.pub_date is not None: @@ -434,6 +438,8 @@ representation of the model from the ``__unicode__()`` method. For example:: + from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -460,6 +466,9 @@ Thus, you should return a nice, human-readable string for the object's The previous :meth:`~Model.__unicode__()` example could be similarly written using ``__str__()`` like this:: + from django.db import models + from django.utils.encoding import force_bytes + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -490,6 +499,7 @@ function is usually the best approach.) For example:: def get_absolute_url(self): + from django.core.urlresolvers import reverse return reverse('people.views.details', args=[str(self.id)]) One place Django uses ``get_absolute_url()`` is in the admin app. If an object diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 5f9316bd2a..90099d13a3 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -145,6 +145,12 @@ Django quotes column and table names behind the scenes. and a question has more than one answer, and the order of answers matters, you'd do this:: + from django.db import models + + class Question(models.Model): + text = models.TextField() + # ... + class Answer(models.Model): question = models.ForeignKey(Question) # ... diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index ffada19082..2788143899 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -232,6 +232,7 @@ the model field that is being aggregated. For example, if you were manipulating a list of blogs, you may want to determine how many entries have been made in each blog:: + >>> from django.db.models import Count >>> q = Blog.objects.annotate(Count('entry')) # The name of the first blog >>> q[0].name @@ -544,6 +545,11 @@ It is an error to pass in ``flat`` when there is more than one field. If you don't pass any values to ``values_list()``, it will return all the fields in the model, in the order they were declared. +Note that this method returns a ``ValuesListQuerySet``. This class behaves +like a list. Most of the time this is enough, but if you require an actual +Python list object, you can simply call ``list()`` on it, which will evaluate +the queryset. + dates ~~~~~ @@ -694,6 +700,8 @@ And here's ``select_related`` lookup:: ``select_related()`` follows foreign keys as far as possible. If you have the following models:: + from django.db import models + class City(models.Model): # ... pass @@ -766,6 +774,13 @@ You can also refer to the reverse direction of a is defined. Instead of specifying the field name, use the :attr:`related_name <django.db.models.ForeignKey.related_name>` for the field on the related object. +.. versionadded:: 1.6 + +If you need to clear the list of related fields added by past calls of +``select_related`` on a ``QuerySet``, you can pass ``None`` as a parameter:: + + >>> without_relations = queryset.select_related(None) + .. deprecated:: 1.5 The ``depth`` parameter to ``select_related()`` has been deprecated. You should replace it with the use of the ``(*fields)`` listing specific @@ -809,6 +824,8 @@ that are supported by ``select_related``. It also supports prefetching of For example, suppose you have these models:: + from django.db import models + class Topping(models.Model): name = models.CharField(max_length=30) @@ -1333,8 +1350,12 @@ get_or_create .. method:: get_or_create(**kwargs) -A convenience method for looking up an object with the given kwargs, creating -one if necessary. +A convenience method for looking up an object with the given kwargs (may be +empty if your model has defaults for all fields), creating one if necessary. + +.. versionchanged:: 1.6 + + Older versions of Django required ``kwargs``. Returns a tuple of ``(object, created)``, where ``object`` is the retrieved or created object and ``created`` is a boolean specifying whether a new object was @@ -1399,6 +1420,41 @@ has a side effect on your data. For more, see `Safe methods`_ in the HTTP spec. .. _Safe methods: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 +.. warning:: + + You can use ``get_or_create()`` through :class:`~django.db.models.ManyToManyField` + attributes and reverse relations. In that case you will restrict the queries + inside the context of that relation. That could lead you to some integrity + problems if you don't use it consistently. + + Being the following models:: + + class Chapter(models.Model): + title = models.CharField(max_length=255, unique=True) + + class Book(models.Model): + title = models.CharField(max_length=256) + chapters = models.ManyToManyField(Chapter) + + You can use ``get_or_create()`` through Book's chapters field, but it only + fetches inside the context of that book:: + + >>> book = Book.objects.create(title="Ulysses") + >>> book.chapters.get_or_create(title="Telemachus") + (<Chapter: Telemachus>, True) + >>> book.chapters.get_or_create(title="Telemachus") + (<Chapter: Telemachus>, False) + >>> Chapter.objects.create(title="Chapter 1") + <Chapter: Chapter 1> + >>> book.chapters.get_or_create(title="Chapter 1") + # Raises IntegrityError + + This is happening because it's trying to get or create "Chapter 1" through the + book "Ulysses", but it can't do any of them: the relation can't fetch that + chapter because it isn't related to that book, but it can't create it either + because ``title`` field should be unique. + + bulk_create ~~~~~~~~~~~ @@ -1540,6 +1596,36 @@ earliest Works otherwise like :meth:`~django.db.models.query.QuerySet.latest` except the direction is changed. +first +~~~~~ +.. method:: first() + +.. versionadded:: 1.6 + +Returns the first object matched by the queryset, or ``None`` if there +is no matching object. If the ``QuerySet`` has no ordering defined, then the +queryset is automatically ordered by the primary key. + +Example:: + + p = Article.objects.order_by('title', 'pub_date').first() + +Note that ``first()`` is a convenience method, the following code sample is +equivalent to the above example:: + + try: + p = Article.objects.order_by('title', 'pub_date')[0] + except IndexError: + p = None + +last +~~~~ +.. method:: last() + +.. versionadded:: 1.6 + +Works like :meth:`first()`, but returns the last object in the queryset. + aggregate ~~~~~~~~~ @@ -1560,6 +1646,7 @@ aggregated. For example, when you are working with blog entries, you may want to know the number of authors that have contributed blog entries:: + >>> from django.db.models import Count >>> q = Blog.objects.aggregate(Count('entry')) {'entry__count': 16} @@ -2037,6 +2124,7 @@ Range test (inclusive). Example:: + import datetime start_date = datetime.date(2005, 1, 1) end_date = datetime.date(2005, 3, 31) Entry.objects.filter(pub_date__range=(start_date, end_date)) diff --git a/docs/ref/models/relations.txt b/docs/ref/models/relations.txt index c923961a19..ffebe37193 100644 --- a/docs/ref/models/relations.txt +++ b/docs/ref/models/relations.txt @@ -12,8 +12,11 @@ Related objects reference * The "other side" of a :class:`~django.db.models.ForeignKey` relation. That is:: + from django.db import models + class Reporter(models.Model): - ... + # ... + pass class Article(models.Model): reporter = models.ForeignKey(Reporter) @@ -24,7 +27,8 @@ Related objects reference * Both sides of a :class:`~django.db.models.ManyToManyField` relation:: class Topping(models.Model): - ... + # ... + pass class Pizza(models.Model): toppings = models.ManyToManyField(Topping) diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 2fac7f2f9c..578418b4ee 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -578,20 +578,27 @@ streaming response if (and only if) no middleware accesses the instantiated with an iterator. Django will consume and save the content of the iterator on first access. -Setting headers -~~~~~~~~~~~~~~~ +Setting header fields +~~~~~~~~~~~~~~~~~~~~~ -To set or remove a header in your response, treat it like a dictionary:: +To set or remove a header field in your response, treat it like a dictionary:: >>> response = HttpResponse() - >>> response['Cache-Control'] = 'no-cache' - >>> del response['Cache-Control'] + >>> response['Age'] = 120 + >>> del response['Age'] Note that unlike a dictionary, ``del`` doesn't raise ``KeyError`` if the header -doesn't exist. +field doesn't exist. -HTTP headers cannot contain newlines. An attempt to set a header containing a -newline character (CR or LF) will raise ``BadHeaderError`` +For setting the ``Cache-Control`` and ``Vary`` header fields, it is recommended +to use the :func:`~django.utils.cache.patch_cache_control` and +:func:`~django.utils.cache.patch_vary_headers` methods from +:mod:`django.utils.cache`, since these fields can have multiple, comma-separated +values. The "patch" methods ensure that other values, e.g. added by a +middleware, are not removed. + +HTTP header fields cannot contain newlines. An attempt to set a header field +containing a newline character (CR or LF) will raise ``BadHeaderError`` Telling the browser to treat the response as a file attachment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -616,7 +623,13 @@ Attributes .. attribute:: HttpResponse.status_code - The `HTTP Status code`_ for the response. + The `HTTP status code`_ for the response. + +.. attribute:: HttpResponse.reason_phrase + + .. versionadded:: 1.6 + + The HTTP reason phrase for the response. .. attribute:: HttpResponse.streaming @@ -628,7 +641,7 @@ Attributes Methods ------- -.. method:: HttpResponse.__init__(content='', content_type=None, status=200) +.. method:: HttpResponse.__init__(content='', content_type=None, status=200, reason=None) Instantiates an ``HttpResponse`` object with the given page content and content type. @@ -646,8 +659,12 @@ Methods Historically, this parameter was called ``mimetype`` (now deprecated). - ``status`` is the `HTTP Status code`_ for the response. + ``status`` is the `HTTP status code`_ for the response. + .. versionadded:: 1.6 + + ``reason`` is the HTTP response phrase. If not provided, a default phrase + will be used. .. method:: HttpResponse.__setitem__(header, value) @@ -727,8 +744,7 @@ Methods This method makes an :class:`HttpResponse` instance a file-like object. -.. _HTTP Status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 - +.. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 .. _ref-httpresponse-subclasses: @@ -851,7 +867,13 @@ Attributes .. attribute:: HttpResponse.status_code - The `HTTP Status code`_ for the response. + The `HTTP status code`_ for the response. + +.. attribute:: HttpResponse.reason_phrase + + .. versionadded:: 1.6 + + The HTTP reason phrase for the response. .. attribute:: HttpResponse.streaming diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index eb470cdd14..ef52d3170c 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -280,6 +280,12 @@ CACHE_MIDDLEWARE_ANONYMOUS_ONLY Default: ``False`` +.. deprecated:: 1.6 + + This setting was largely ineffective because of using cookies for sessions + and CSRF. See the :doc:`Django 1.6 release notes</releases/1.6>` for more + information. + If the value of this setting is ``True``, only anonymous requests (i.e., not those made by a logged-in user) will be cached. Otherwise, the middleware caches every page that doesn't have GET or POST parameters. @@ -287,8 +293,6 @@ caches every page that doesn't have GET or POST parameters. If you set the value of this setting to ``True``, you should make sure you've activated ``AuthenticationMiddleware``. -See :doc:`/topics/cache`. - .. setting:: CACHE_MIDDLEWARE_KEY_PREFIX CACHE_MIDDLEWARE_KEY_PREFIX @@ -340,9 +344,9 @@ CSRF_COOKIE_HTTPONLY Default: ``False`` -Whether to use HttpOnly flag on the CSRF cookie. If this is set to ``True``, -client-side JavaScript will not to be able to access the CSRF cookie. See -:setting:`SESSION_COOKIE_HTTPONLY` for details on HttpOnly. +Whether to use ``HttpOnly`` flag on the CSRF cookie. If this is set to +``True``, client-side JavaScript will not to be able to access the CSRF cookie. +See :setting:`SESSION_COOKIE_HTTPONLY` for details on ``HttpOnly``. .. setting:: CSRF_COOKIE_NAME @@ -1239,7 +1243,7 @@ Default: ``()`` (Empty tuple) A tuple of IP addresses, as strings, that: * See debug comments, when :setting:`DEBUG` is ``True`` -* Receive X headers if the ``XViewMiddleware`` is installed (see +* Receive X headers in admindocs if the ``XViewMiddleware`` is installed (see :doc:`/topics/http/middleware`) .. setting:: LANGUAGE_CODE @@ -2227,6 +2231,9 @@ Controls where Django stores message data. Valid values are: See :ref:`message storage backends <message-storage-backends>` for more details. +The backends that use cookies -- ``CookieStorage`` and ``FallbackStorage`` -- +use the value of :setting:`SESSION_COOKIE_DOMAIN` when setting their cookies. + .. setting:: MESSAGE_TAGS MESSAGE_TAGS @@ -2258,18 +2265,6 @@ to override. See :ref:`message-displaying` above for more details. according to the values in the above :ref:`constants table <message-level-constants>`. -.. _messages-session_cookie_domain: - -SESSION_COOKIE_DOMAIN ---------------------- - -Default: ``None`` - -The storage backends that use cookies -- ``CookieStorage`` and -``FallbackStorage`` -- use the value of :setting:`SESSION_COOKIE_DOMAIN` in -setting their cookies. - - .. _settings-sessions: Sessions @@ -2320,7 +2315,7 @@ SESSION_COOKIE_HTTPONLY Default: ``True`` -Whether to use HTTPOnly flag on the session cookie. If this is set to +Whether to use ``HTTPOnly`` flag on the session cookie. If this is set to ``True``, client-side JavaScript will not to be able to access the session cookie. diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index ca472bd60e..e7270e1957 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -360,6 +360,53 @@ Management signals Signals sent by :doc:`django-admin </ref/django-admin>`. +pre_syncdb +---------- + +.. data:: django.db.models.signals.pre_syncdb + :module: + +Sent by the :djadmin:`syncdb` command before it starts to install an +application. + +Any handlers that listen to this signal need to be written in a particular +place: a ``management`` module in one of your :setting:`INSTALLED_APPS`. If +handlers are registered anywhere else they may not be loaded by +:djadmin:`syncdb`. + +Arguments sent with this signal: + +``sender`` + The ``models`` module that was just installed. That is, if + :djadmin:`syncdb` just installed an app called ``"foo.bar.myapp"``, + ``sender`` will be the ``foo.bar.myapp.models`` module. + +``app`` + Same as ``sender``. + +``create_models`` + A list of the model classes from any app which :djadmin:`syncdb` plans to + create. + + +``verbosity`` + Indicates how much information manage.py is printing on screen. See + the :djadminopt:`--verbosity` flag for details. + + Functions which listen for :data:`pre_syncdb` should adjust what they + output to the screen based on the value of this argument. + +``interactive`` + If ``interactive`` is ``True``, it's safe to prompt the user to input + things on the command line. If ``interactive`` is ``False``, functions + which listen for this signal should not try to prompt for anything. + + For example, the :mod:`django.contrib.auth` app only prompts to create a + superuser when ``interactive`` is ``True``. + +``db`` + The alias of database on which a command will operate. + post_syncdb ----------- diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index cdefe2fae8..4f34d150ed 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -215,6 +215,7 @@ re-rendered, you can re-evaluate the rendered content, and assign the content of the response manually:: # Set up a rendered TemplateResponse + >>> from django.template.response import TemplateResponse >>> t = TemplateResponse(request, 'original.html', {}) >>> t.render() >>> print(t.content) @@ -256,6 +257,8 @@ To define a post-render callback, just define a function that takes a single argument -- response -- and register that function with the template response:: + from django.template.response import TemplateResponse + def my_render_callback(response): # Do content-sensitive processing do_post_processing() diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 677aa13cbb..160cdc7194 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -286,6 +286,7 @@ fully-populated dictionary to ``Context()``. But you can add and delete items from a ``Context`` object once it's been instantiated, too, using standard dictionary syntax:: + >>> from django.template import Context >>> c = Context({"foo": "bar"}) >>> c['foo'] 'bar' @@ -397,6 +398,9 @@ Also, you can give ``RequestContext`` a list of additional processors, using the optional, third positional argument, ``processors``. In this example, the ``RequestContext`` instance gets a ``ip_address`` variable:: + from django.http import HttpResponse + from django.template import RequestContext + def ip_address_processor(request): return {'ip_address': request.META['REMOTE_ADDR']} @@ -417,6 +421,9 @@ optional, third positional argument, ``processors``. In this example, the :func:`~django.shortcuts.render_to_response()`: a ``RequestContext`` instance. Your code might look like this:: + from django.shortcuts import render_to_response + from django.template import RequestContext + def some_view(request): # ... return render_to_response('my_template.html', diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 287fd4f59e..24eda2ce2c 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -862,6 +862,8 @@ above would result in the following output: * New York: 20,000,000 * India * Calcutta: 15,000,000 +* USA + * Chicago: 7,000,000 * Japan * Tokyo: 33,000,000 diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 14ae9aa9b8..d2ef945a2e 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -490,7 +490,7 @@ Atom1Feed Usually you should build up HTML using Django's templates to make use of its autoescape mechanism, using the utilities in :mod:`django.utils.safestring` -where appropriate. This module provides some additional low level utilitiesfor +where appropriate. This module provides some additional low level utilities for escaping HTML. .. function:: escape(text) @@ -564,7 +564,13 @@ escaping HTML. strip_tags(value) If ``value`` is ``"<b>Joel</b> <button>is</button> a <span>slug</span>"`` the - return value will be ``"Joel is a slug"``. + return value will be ``"Joel is a slug"``. Note that ``strip_tags`` result + may still contain unsafe HTML content, so you might use + :func:`~django.utils.html.escape` to make it a safe string. + + .. versionchanged:: 1.6 + + For improved safety, ``strip_tags`` is now parser-based. .. function:: remove_tags(value, tags) @@ -923,9 +929,21 @@ For a complete discussion on the usage of the following see the .. function:: now() - Returns an aware or naive :class:`~datetime.datetime` that represents the - current point in time when :setting:`USE_TZ` is ``True`` or ``False`` - respectively. + Returns a :class:`~datetime.datetime` that represents the + current point in time. Exactly what's returned depends on the value of + :setting:`USE_TZ`: + + * If :setting:`USE_TZ` is ``False``, this will be be a + :ref:`naive <naive_vs_aware_datetimes>` datetime (i.e. a datetime + without an associated timezone) that represents the current time + in the system's local timezone. + + * If :setting:`USE_TZ` is ``True``, this will be an + :ref:`aware <naive_vs_aware_datetimes>` datetime representing the + current time in UTC. Note that :func:`now` will always return + times in UTC regardless of the value of :setting:`TIME_ZONE`; + you can use :func:`localtime` to convert to a time in the current + time zone. .. function:: is_aware(value) diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index 42947d9a44..634e6afaf2 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -154,7 +154,7 @@ requests. These include: requests in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 89cece941b..45ebb2f1fe 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -299,7 +299,7 @@ requests. These include: in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. * Support for lookups spanning relations in admin's diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 83a5f54fc7..a013665ad3 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -541,8 +541,8 @@ compare HTML directly with the new :meth:`~django.test.SimpleTestCase.assertHTMLEqual` and :meth:`~django.test.SimpleTestCase.assertHTMLNotEqual` assertions, or use the ``html=True`` flag with -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains` to test whether the +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains` to test whether the client's response contains a given HTML fragment. See the :ref:`assertions documentation <assertions>` for more. @@ -1093,8 +1093,8 @@ wild, because they would confuse browsers too. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's now possible to check whether a template was used within a block of -code with :meth:`~django.test.TestCase.assertTemplateUsed` and -:meth:`~django.test.TestCase.assertTemplateNotUsed`. And they +code with :meth:`~django.test.SimpleTestCase.assertTemplateUsed` and +:meth:`~django.test.SimpleTestCase.assertTemplateNotUsed`. And they can be used as a context manager:: with self.assertTemplateUsed('index.html'): diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 98889254cd..6736af8c2d 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -78,10 +78,10 @@ location of tests. The previous runner ``models.py`` and ``tests.py`` modules of a Python package in :setting:`INSTALLED_APPS`. -The new runner (``django.test.runner.DjangoTestDiscoverRunner``) uses the test -discovery features built into unittest2 (the version of unittest in the Python -2.7+ standard library, and bundled with Django). With test discovery, tests can -be located in any module whose name matches the pattern ``test*.py``. +The new runner (``django.test.runner.DiscoverRunner``) uses the test discovery +features built into ``unittest2`` (the version of ``unittest`` in the +Python 2.7+ standard library, and bundled with Django). With test discovery, +tests can be located in any module whose name matches the pattern ``test*.py``. In addition, the test labels provided to ``./manage.py test`` to nominate specific tests to run must now be full Python dotted paths (or directory @@ -111,7 +111,7 @@ Django 1.6 adds support for savepoints in SQLite, with some :ref:`limitations ``BinaryField`` model field ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A new :class:`django.db.models.BinaryField` model field allows to store raw +A new :class:`django.db.models.BinaryField` model field allows storage of raw binary data in the database. GeoDjango form widgets @@ -127,13 +127,13 @@ Minor features * Authentication backends can raise ``PermissionDenied`` to immediately fail the authentication chain. -* The HttpOnly flag can be set on the CSRF cookie with +* The ``HttpOnly`` flag can be set on the CSRF cookie with :setting:`CSRF_COOKIE_HTTPONLY`. -* The ``assertQuerysetEqual()`` now checks for undefined order and raises - ``ValueError`` if undefined order is spotted. The order is seen as - undefined if the given ``QuerySet`` isn't ordered and there are more than - one ordered values to compare against. +* The :meth:`~django.test.TransactionTestCase.assertQuerysetEqual` now checks + for undefined order and raises :exc:`~exceptions.ValueError` if undefined + order is spotted. The order is seen as undefined if the given ``QuerySet`` + isn't ordered and there are more than one ordered values to compare against. * Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with :meth:`~django.db.models.query.QuerySet.latest`. @@ -146,10 +146,10 @@ Minor features * The default widgets for :class:`~django.forms.EmailField`, :class:`~django.forms.URLField`, :class:`~django.forms.IntegerField`, :class:`~django.forms.FloatField` and :class:`~django.forms.DecimalField` use - the new type attributes available in HTML5 (type='email', type='url', - type='number'). Note that due to erratic support of the ``number`` input type - with localized numbers in current browsers, Django only uses it when numeric - fields are not localized. + the new type attributes available in HTML5 (``type='email'``, ``type='url'``, + ``type='number'``). Note that due to erratic support of the ``number`` + input type with localized numbers in current browsers, Django only uses it + when numeric fields are not localized. * The ``number`` argument for :ref:`lazy plural translations <lazy-plural-translations>` can be provided at translation time rather than @@ -185,19 +185,21 @@ Minor features * The jQuery library embedded in the admin has been upgraded to version 1.9.1. * Syndication feeds (:mod:`django.contrib.syndication`) can now pass extra - context through to feed templates using a new `Feed.get_context_data()` - callback. + context through to feed templates using a new + :meth:`Feed.get_context_data() + <django.contrib.syndication.Feed.get_context_data>` callback. * The admin list columns have a ``column-<field_name>`` class in the HTML so the columns header can be styled with CSS, e.g. to set a column width. -* The isolation level can be customized under PostgreSQL. +* The :ref:`isolation level<database-isolation-level>` can be customized under + PostgreSQL. * The :ttag:`blocktrans` template tag now respects :setting:`TEMPLATE_STRING_IF_INVALID` for variables not present in the context, just like other template constructs. -* SimpleLazyObjects will now present more helpful representations in shell +* ``SimpleLazyObject``\s will now present more helpful representations in shell debugging situations. * Generic :class:`~django.contrib.gis.db.models.GeometryField` is now editable @@ -210,7 +212,7 @@ Minor features * The documentation contains a :doc:`deployment checklist </howto/deployment/checklist>`. -* The :djadmin:`diffsettings` comand gained a ``--all`` option. +* The :djadmin:`diffsettings` command gained a ``--all`` option. * ``django.forms.fields.Field.__init__`` now calls ``super()``, allowing field mixins to implement ``__init__()`` methods that will reliably be @@ -234,6 +236,73 @@ Minor features .. _`Pillow`: https://pypi.python.org/pypi/Pillow .. _`PIL`: https://pypi.python.org/pypi/PIL +* :doc:`ModelForm </topics/forms/modelforms/>` accepts a new + Meta option: ``localized_fields``. Fields included in this list will be localized + (by setting ``localize`` on the form field). + +* The ``choices`` argument to model fields now accepts an iterable of iterables + instead of requiring an iterable of lists or tuples. + +* The reason phrase can be customized in HTTP responses using + :attr:`~django.http.HttpResponse.reason_phrase`. + +* When giving the URL of the next page for + :func:`~django.contrib.auth.views.logout`, + :func:`~django.contrib.auth.views.password_reset`, + :func:`~django.contrib.auth.views.password_reset_confirm`, + and :func:`~django.contrib.auth.views.password_change`, you can now pass + URL names and they will be resolved. + +* The :djadmin:`dumpdata` ``manage.py`` command now has a :djadminopt:`--pks` + option which will allow users to specify the primary keys of objects they + want to dump. This option can only be used with one model. + +* Added ``QuerySet`` methods :meth:`~django.db.models.query.QuerySet.first` + and :meth:`~django.db.models.query.QuerySet.last` which are convenience + methods returning the first or last object matching the filters. Returns + ``None`` if there are no objects matching. + +* :class:`~django.views.generic.base.View` and + :class:`~django.views.generic.base.RedirectView` now support HTTP ``PATCH`` + method. + +* :class:`GenericForeignKey <django.contrib.contenttypes.generic.GenericForeignKey>` + now takes an optional + :attr:`~django.contrib.contenttypes.generic.GenericForeignKey.for_concrete_model` + argument, which when set to ``False`` allows the field to reference proxy + models. The default is ``True`` to retain the old behavior. + +* The :class:`~django.middleware.locale.LocaleMiddleware` now stores the active + language in session if it is not present there. This prevents loss of + language settings after session flush, e.g. logout. + +* :exc:`~django.core.exceptions.SuspiciousOperation` has been differentiated + into a number of subclasses, and each will log to a matching named logger + under the ``django.security`` logging hierarchy. Along with this change, + a ``handler400`` mechanism and default view are used whenever + a ``SuspiciousOperation`` reaches the WSGI handler to return an + ``HttpResponseBadRequest``. + +* The :exc:`~django.core.exceptions.DoesNotExist` exception now includes a + message indicating the name of the attribute used for the lookup. + +* The :meth:`~django.db.models.query.QuerySet.get_or_create` method no longer + requires at least one keyword argument. + +* The :class:`~django.test.SimpleTestCase` class includes a new assertion + helper for testing formset errors: + :meth:`~django.test.SimpleTestCase.assertFormsetError`. + +* The list of related fields added to a + :class:`~django.db.models.query.QuerySet` by + :meth:`~django.db.models.query.QuerySet.select_related` can be cleared using + ``select_related(None)``. + +* The :meth:`~django.contrib.admin.InlineModelAdmin.get_extra` and + :meth:`~django.contrib.admin.InlineModelAdmin.get_max_num` methods on + :class:`~django.contrib.admin.InlineModelAdmin` may be overridden to + customize the extra and maximum number of inline forms. + Backwards incompatible changes in 1.6 ===================================== @@ -253,7 +322,7 @@ Behavior changes Database-level autocommit is enabled by default in Django 1.6. While this doesn't change the general spirit of Django's transaction management, there -are a few known backwards-incompatibities, described in the :ref:`transaction +are a few known backwards-incompatibilities, described in the :ref:`transaction management docs <transactions-upgrading-from-1.5>`. You should review your code to determine if you're affected. @@ -264,9 +333,10 @@ The changes in transaction management may result in additional statements to create, release or rollback savepoints. This is more likely to happen with SQLite, since it didn't support savepoints until this release. -If tests using :meth:`~django.test.TestCase.assertNumQueries` fail because of -a higher number of queries than expected, check that the extra queries are -related to savepoints, and adjust the expected number of queries accordingly. +If tests using :meth:`~django.test.TransactionTestCase.assertNumQueries` fail +because of a higher number of queries than expected, check that the extra +queries are related to savepoints, and adjust the expected number of queries +accordingly. Autocommit option for PostgreSQL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -291,21 +361,15 @@ support some types of tests that were supported by the previous runner: your test suite, follow the `recommendations in the Python documentation`_. Django bundles a modified version of the :mod:`doctest` module from the Python -standard library (in ``django.test._doctest``) in order to allow passing in a -custom ``DocTestRunner`` when instantiating a ``DocTestSuite``, and includes -some additional doctest utilities (``django.test.testcases.DocTestRunner`` -turns on the ``ELLIPSIS`` option by default, and -``django.test.testcases.OutputChecker`` provides better matching of XML, JSON, -and numeric data types). - -These utilities are deprecated and will be removed in Django 1.8; doctest -suites should be updated to work with the standard library's doctest module (or -converted to unittest-compatible tests). +standard library (in ``django.test._doctest``) and includes some additional +doctest utilities. These utilities are deprecated and will be removed in Django +1.8; doctest suites should be updated to work with the standard library's +doctest module (or converted to unittest-compatible tests). If you wish to delay updates to your test suite, you can set your :setting:`TEST_RUNNER` setting to ``django.test.simple.DjangoTestSuiteRunner`` -to fully restore the old test behavior. ``DjangoTestSuiteRunner`` is -deprecated but will not be removed from Django until version 1.8. +to fully restore the old test behavior. ``DjangoTestSuiteRunner`` is deprecated +but will not be removed from Django until version 1.8. .. _recommendations in the Python documentation: http://docs.python.org/2/library/doctest.html#unittest-api @@ -395,10 +459,9 @@ they are located after a ``{#`` / ``#}``-type comment on the same line. E.g.: Location of translator comments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Validation of the placement of :ref:`translator-comments-in-templates` -specified using ``{#`` / ``#}`` is now stricter. All translator comments not -located at the end of their respective lines in a template are ignored and a -warning is generated by :djadmin:`makemessages` when it finds them. E.g.: +:ref:`translator-comments-in-templates` specified using ``{#`` / ``#}`` need to +be at the end of a line. If they are not, the comments are ignored and +:djadmin:`makemessages` will generate a warning. For example: .. code-block:: html+django @@ -439,7 +502,7 @@ For Oracle, execute this query: ALTER TABLE DJANGO_COMMENTS MODIFY (ip_address VARCHAR2(39)); -If you do not apply this change, the behaviour is unchanged: on MySQL, IPv6 +If you do not apply this change, the behavior is unchanged: on MySQL, IPv6 addresses are silently truncated; on Oracle, an exception is generated. No database change is needed for SQLite or PostgreSQL databases. @@ -461,6 +524,63 @@ parameters. For example:: ``SQLite`` users need to check and update such queries. +.. _m2m-help_text: + +Help text of model form fields for ManyToManyField fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +HTML rendering of model form fields corresponding to +:class:`~django.db.models.ManyToManyField` ORM model fields used to get the +hard-coded sentence + + *Hold down "Control", or "Command" on a Mac, to select more than one.* + +(or its translation to the active locale) imposed as the help legend shown along +them if neither :attr:`model <django.db.models.Field.help_text>` nor :attr:`form +<django.forms.Field.help_text>` ``help_text`` attribute was specified by the +user (or appended to, if ``help_text`` was provided.) + +This happened always, possibly even with form fields implementing user +interactions that don't involve a keyboard and/or a mouse and was handled at the +model field layer. + +Starting with Django 1.6 this doesn't happen anymore. + +The change can affect you in a backward incompatible way if you employ custom +model form fields and/or widgets for ``ManyToManyField`` model fields whose UIs +do rely on the automatic provision of the mentioned hard-coded sentence. These +form field implementations need to adapt to the new scenario by providing their +own handling of the ``help_text`` attribute. + +Applications that use Django :doc:`model form </topics/forms/modelforms>` +facilities together with Django built-in form :doc:`fields </ref/forms/fields>` +and :doc:`widgets </ref/forms/widgets>` aren't affected but need to be aware of +what's described in :ref:`m2m-help_text-deprecation` below. + +This is because, as an ad-hoc temporary backward-compatibility provision, the +described non-standard behavior has been preserved but moved to the model form +field layer and occurs only when the associated widget is +:class:`~django.forms.SelectMultiple` or selected subclasses. + +QuerySet iteration +~~~~~~~~~~~~~~~~~~ + +The ``QuerySet`` iteration was changed to immediately convert all fetched +rows to ``Model`` objects. In Django 1.5 and earlier the fetched rows were +converted to ``Model`` objects in chunks of 100. + +Existing code will work, but the amount of rows converted to objects +might change in certain use cases. Such usages include partially looping +over a queryset or any usage which ends up doing ``__bool__`` or +``__contains__``. + +Notably most database backends did fetch all the rows in one go already in +1.5. + +It is still possible to convert the fetched rows to ``Model`` objects +lazily by using the :meth:`~django.db.models.query.QuerySet.iterator()` +method. + Miscellaneous ~~~~~~~~~~~~~ @@ -481,6 +601,39 @@ Miscellaneous changes in 1.6 particularly affect :class:`~django.forms.DecimalField` and :class:`~django.forms.ModelMultipleChoiceField`. +* There have been changes in the way timeouts are handled in cache backends. + Explicitly passing in ``timeout=None`` no longer results in using the + default timeout. It will now set a non-expiring timeout. Passing 0 into the + memcache backend no longer uses the default timeout, and now will + set-and-expire-immediately the value. + +* The ``django.contrib.flatpages`` app used to set custom HTTP headers for + debugging purposes. This functionality was not documented and made caching + ineffective so it has been removed, along with its generic implementation, + previously available in ``django.core.xheaders``. + +* The ``XViewMiddleware`` has been moved from ``django.middleware.doc`` to + ``django.contrib.admindocs.middleware`` because it is an implementation + detail of admindocs, proven not to be reusable in general. + +* :class:`~django.db.models.GenericIPAddressField` will now only allow + ``blank`` values if ``null`` values are also allowed. Creating a + ``GenericIPAddressField`` where ``blank`` is allowed but ``null`` is not + will trigger a model validation error because ``blank`` values are always + stored as ``null``. Previously, storing a ``blank`` value in a field which + did not allow ``null`` would cause a database exception at runtime. + +* If a :class:`~django.core.urlresolvers.NoReverseMatch` exception is raised + from a method when rendering a template, it is not silenced. For example, + ``{{ obj.view_href }}`` will cause template rendering to fail if + ``view_href()`` raises ``NoReverseMatch``. There is no change to the + ``{% url %}`` tag, it causes template rendering to fail like always when + ``NoReverseMatch`` is risen. + +* :meth:`django.test.client.Client.logout` now calls + :meth:`django.contrib.auth.logout` which will send the + :func:`~django.contrib.auth.signals.user_logged_out` signal. + Features deprecated in 1.6 ========================== @@ -551,6 +704,23 @@ If necessary, you can temporarily disable auto-escaping with :func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %} <autoescape>`. +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CacheMiddleware`` used to provide a way to cache requests only if they +weren't made by a logged-in user. This mechanism was largely ineffective +because the middleware correctly takes into account the ``Vary: Cookie`` HTTP +header, and this header is being set on a variety of occasions, such as: + +* accessing the session, or +* using CSRF protection, which is turned on by default, or +* using a client-side library which sets cookies, like `Google Analytics`__. + +This makes the cache effectively work on a per-session basis regardless of the +``CACHE_MIDDLEWARE_ANONYMOUS_ONLY`` setting. + +__ http://www.google.com/analytics/ + ``SEND_BROKEN_LINK_EMAILS`` setting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -575,7 +745,7 @@ from your settings. If you defined your own form widgets and defined the ``_has_changed`` method on a widget, you should now define this method on the form field itself. -``module_name`` model meta attribute +``module_name`` model _meta attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``Model._meta.module_name`` was renamed to ``model_name``. Despite being a @@ -617,7 +787,7 @@ particular with boolean fields, it is possible for this problem to be completely invisible. This is a form of `Mass assignment vulnerability <http://en.wikipedia.org/wiki/Mass_assignment_vulnerability>`_. -For this reason, this behaviour is deprecated, and using the ``Meta.exclude`` +For this reason, this behavior is deprecated, and using the ``Meta.exclude`` option is strongly discouraged. Instead, all fields that are intended for inclusion in the form should be listed explicitly in the ``fields`` attribute. @@ -636,7 +806,7 @@ is another option. The admin has its own methods for defining fields redundant. Instead, simply omit the ``Meta`` inner class of the ``ModelForm``, or omit the ``Meta.model`` attribute. Since the ``ModelAdmin`` subclass knows which model it is for, it can add the necessary attributes to derive a -functioning ``ModelForm``. This behaviour also works for earlier Django +functioning ``ModelForm``. This behavior also works for earlier Django versions. ``UpdateView`` and ``CreateView`` without explicit fields @@ -655,3 +825,16 @@ you can set set the ``form_class`` attribute to a ``ModelForm`` that explicitly defines the fields to be used. Defining an ``UpdateView`` or ``CreateView`` subclass to be used with a model but without an explicit list of fields is deprecated. + +.. _m2m-help_text-deprecation: + +Munging of help text of model form fields for ``ManyToManyField`` fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All special handling of the ``help_text`` attribute of ``ManyToManyField`` model +fields performed by standard model or model form fields as described in +:ref:`m2m-help_text` above is deprecated and will be removed in Django 1.8. + +Help text of these fields will need to be handled either by applications, custom +form fields or widgets, just like happens with the rest of the model field +types. diff --git a/docs/releases/index.txt b/docs/releases/index.txt index c5afd8c719..85b3d211a8 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -6,10 +6,11 @@ Release notes for the official Django releases. Each release note will tell you what's new in each version, and will also describe any backwards-incompatible changes made in that version. -For those upgrading to a new version of Django, you will need to check -all the backwards-incompatible changes and deprecated features for -each 'final' release from the one after your current Django version, -up to and including the new version. +For those :doc:`upgrading to a new version of Django</howto/upgrade-version>`, +you will need to check all the backwards-incompatible changes and +:doc:`deprecated features</internals/deprecation>` for each 'final' release +from the one after your current Django version, up to and including the new +version. Final releases ============== diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 56f3e60350..bc021b14ad 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -939,7 +939,7 @@ authentication app:: raise ValueError('Users must have an email address') user = self.model( - email=MyUserManager.normalize_email(email), + email=self.normalize_email(email), date_of_birth=date_of_birth, ) @@ -1075,7 +1075,6 @@ code would be required in the app's ``admin.py`` file:: (None, {'fields': ('email', 'password')}), ('Personal info', {'fields': ('date_of_birth',)}), ('Permissions', {'fields': ('is_admin',)}), - ('Important dates', {'fields': ('last_login',)}), ) add_fieldsets = ( (None, { @@ -1092,3 +1091,8 @@ code would be required in the app's ``admin.py`` file:: # ... and, since we're not using Django's builtin permissions, # unregister the Group model from admin. admin.site.unregister(Group) + +Finally, specify the custom model as the default user model for your project +using the :setting:`AUTH_USER_MODEL` setting in your ``settings.py``:: + + AUTH_USER_MODEL = 'customauth.MyUser' diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 6b6d57511a..2352770bad 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -443,15 +443,9 @@ Then, add the following required settings to your Django settings file: The cache middleware caches GET and HEAD responses with status 200, where the request and response headers allow. Responses to requests for the same URL with different query parameters are considered to be unique pages and are cached separately. -Optionally, if the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` -setting is ``True``, only anonymous requests (i.e., not those made by a -logged-in user) will be cached. This is a simple and effective way of disabling -caching for any user-specific pages (including Django's admin interface). Note -that if you use :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY`, you should make -sure you've activated ``AuthenticationMiddleware``. The cache middleware -expects that a HEAD request is answered with the same response headers as -the corresponding GET request; in which case it can return a cached GET -response for HEAD request. +The cache middleware expects that a HEAD request is answered with the same +response headers as the corresponding GET request; in which case it can return +a cached GET response for HEAD request. Additionally, the cache middleware automatically sets a few headers in each :class:`~django.http.HttpResponse`: @@ -707,10 +701,15 @@ The basic interface is ``set(key, value, timeout)`` and ``get(key)``:: >>> cache.get('my_key') 'hello, world!' -The ``timeout`` argument is optional and defaults to the ``timeout`` -argument of the appropriate backend in the :setting:`CACHES` setting -(explained above). It's the number of seconds the value should be stored -in the cache. +The ``timeout`` argument is optional and defaults to the ``timeout`` argument +of the appropriate backend in the :setting:`CACHES` setting (explained above). +It's the number of seconds the value should be stored in the cache. Passing in +``None`` for ``timeout`` will cache the value forever. + +.. versionchanged:: 1.6 + + Previously, passing ``None`` explicitly would use the default timeout + value. If the object doesn't exist in the cache, ``cache.get()`` returns ``None``:: @@ -998,8 +997,8 @@ produces different content based on some difference in request headers -- such as a cookie, or a language, or a user-agent -- you'll need to use the ``Vary`` header to tell caching mechanisms that the page output depends on those things. -To do this in Django, use the convenient ``vary_on_headers`` view decorator, -like so:: +To do this in Django, use the convenient +:func:`django.views.decorators.vary.vary_on_headers` view decorator, like so:: from django.views.decorators.vary import vary_on_headers @@ -1028,8 +1027,9 @@ the user-agent ``Mozilla`` and the cookie value ``foo=bar`` will be considered different from a request with the user-agent ``Mozilla`` and the cookie value ``foo=ham``. -Because varying on cookie is so common, there's a ``vary_on_cookie`` -decorator. These two views are equivalent:: +Because varying on cookie is so common, there's a +:func:`django.views.decorators.vary.vary_on_cookie` decorator. These two views +are equivalent:: @vary_on_cookie def my_view(request): @@ -1042,7 +1042,7 @@ decorator. These two views are equivalent:: The headers you pass to ``vary_on_headers`` are not case sensitive; ``"User-Agent"`` is the same thing as ``"user-agent"``. -You can also use a helper function, ``django.utils.cache.patch_vary_headers``, +You can also use a helper function, :func:`django.utils.cache.patch_vary_headers`, directly. This function sets, or adds to, the ``Vary header``. For example:: from django.utils.cache import patch_vary_headers @@ -1091,8 +1091,9 @@ exclusive. The decorator ensures that the "public" directive is removed if "private" should be set (and vice versa). An example use of the two directives would be a blog site that offers both private and public entries. Public entries may be cached on any shared cache. The following code uses -``patch_cache_control``, the manual way to modify the cache control header -(it is internally called by the ``cache_control`` decorator):: +:func:`django.utils.cache.patch_cache_control`, the manual way to modify the +cache control header (it is internally called by the ``cache_control`` +decorator):: from django.views.decorators.cache import patch_cache_control from django.views.decorators.vary import vary_on_cookie diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 64b998770f..7ffa471e79 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -248,7 +248,7 @@ specify the objects that the view will operate upon -- you can also specify the list of objects using the ``queryset`` argument:: from django.views.generic import DetailView - from books.models import Publisher, Book + from books.models import Publisher class PublisherDetail(DetailView): @@ -326,6 +326,7 @@ various useful things are stored on ``self``; as well as the request Here, we have a URLconf with a single captured group:: # urls.py + from django.conf.urls import patterns from books.views import PublisherBookList urlpatterns = patterns('', @@ -375,6 +376,7 @@ Imagine we had a ``last_accessed`` field on our ``Author`` object that we were using to keep track of the last time anybody looked at that author:: # models.py + from django.db import models class Author(models.Model): salutation = models.CharField(max_length=10) @@ -390,6 +392,7 @@ updated. First, we'd need to add an author detail bit in the URLconf to point to a custom view:: + from django.conf.urls import patterns, url from books.views import AuthorDetailView urlpatterns = patterns('', @@ -401,7 +404,6 @@ Then we'd write our new view -- ``get_object`` is the method that retrieves the object -- so we simply override it and wrap the call:: from django.views.generic import DetailView - from django.shortcuts import get_object_or_404 from django.utils import timezone from books.models import Author diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 86c5280159..7c4e02cc4e 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -222,6 +222,7 @@ works for AJAX requests as well as 'normal' form POSTs:: from django.http import HttpResponse from django.views.generic.edit import CreateView + from myapp.models import Author class AjaxableResponseMixin(object): """ diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 9550d2fb86..980e571c85 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -258,6 +258,7 @@ mixin. We can hook this into our URLs easily enough:: # urls.py + from django.conf.urls import patterns, url from books.views import RecordInterest urlpatterns = patterns('', @@ -440,6 +441,7 @@ Our new ``AuthorDetail`` looks like this:: from django.core.urlresolvers import reverse from django.views.generic import DetailView from django.views.generic.edit import FormMixin + from books.models import Author class AuthorInterestForm(forms.Form): message = forms.CharField() @@ -546,6 +548,8 @@ template as ``AuthorDisplay`` is using on ``GET``. .. code-block:: python + from django.core.urlresolvers import reverse + from django.http import HttpResponseForbidden from django.views.generic import FormView from django.views.generic.detail import SingleObjectMixin @@ -657,6 +661,8 @@ own version of :class:`~django.views.generic.detail.DetailView` by mixing :class:`~django.views.generic.detail.DetailView` before template rendering behavior has been mixed in):: + from django.views.generic.detail import BaseDetailView + class JSONDetailView(JSONResponseMixin, BaseDetailView): pass @@ -675,6 +681,8 @@ and override the implementation of to defer to the appropriate subclass depending on the type of response that the user requested:: + from django.views.generic.detail import SingleObjectTemplateResponseMixin + class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): def render_to_response(self, context): # Look for a 'format=json' GET argument diff --git a/docs/topics/db/aggregation.txt b/docs/topics/db/aggregation.txt index 125cd0bdee..1024d6b0c2 100644 --- a/docs/topics/db/aggregation.txt +++ b/docs/topics/db/aggregation.txt @@ -18,27 +18,29 @@ used to track the inventory for a series of online bookstores: .. code-block:: python + from django.db import models + class Author(models.Model): - name = models.CharField(max_length=100) - age = models.IntegerField() + name = models.CharField(max_length=100) + age = models.IntegerField() class Publisher(models.Model): - name = models.CharField(max_length=300) - num_awards = models.IntegerField() + name = models.CharField(max_length=300) + num_awards = models.IntegerField() class Book(models.Model): - name = models.CharField(max_length=300) - pages = models.IntegerField() - price = models.DecimalField(max_digits=10, decimal_places=2) - rating = models.FloatField() - authors = models.ManyToManyField(Author) - publisher = models.ForeignKey(Publisher) - pubdate = models.DateField() + name = models.CharField(max_length=300) + pages = models.IntegerField() + price = models.DecimalField(max_digits=10, decimal_places=2) + rating = models.FloatField() + authors = models.ManyToManyField(Author) + publisher = models.ForeignKey(Publisher) + pubdate = models.DateField() class Store(models.Model): - name = models.CharField(max_length=300) - books = models.ManyToManyField(Book) - registered_users = models.PositiveIntegerField() + name = models.CharField(max_length=300) + books = models.ManyToManyField(Book) + registered_users = models.PositiveIntegerField() Cheat sheet =========== @@ -123,7 +125,7 @@ If you want to generate more than one aggregate, you just add another argument to the ``aggregate()`` clause. So, if we also wanted to know the maximum and minimum price of all books, we would issue the query:: - >>> from django.db.models import Avg, Max, Min, Count + >>> from django.db.models import Avg, Max, Min >>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price')) {'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')} @@ -148,6 +150,7 @@ the number of authors: .. code-block:: python # Build an annotated queryset + >>> from django.db.models import Count >>> q = Book.objects.annotate(Count('authors')) # Interrogate the first object in the queryset >>> q[0] @@ -192,6 +195,7 @@ and aggregate the related value. For example, to find the price range of books offered in each store, you could use the annotation:: + >>> from django.db.models import Max, Min >>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price')) This tells Django to retrieve the ``Store`` model, join (through the @@ -222,7 +226,7 @@ For example, we can ask for all publishers, annotated with their respective total book stock counters (note how we use ``'book'`` to specify the ``Publisher`` -> ``Book`` reverse foreign key hop):: - >>> from django.db.models import Count, Min, Sum, Max, Avg + >>> from django.db.models import Count, Min, Sum, Avg >>> Publisher.objects.annotate(Count('book')) (Every ``Publisher`` in the resulting ``QuerySet`` will have an extra attribute @@ -269,6 +273,7 @@ constraining the objects for which an annotation is calculated. For example, you can generate an annotated list of all books that have a title starting with "Django" using the query:: + >>> from django.db.models import Count, Avg >>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors')) When used with an ``aggregate()`` clause, a filter has the effect of @@ -407,6 +412,8 @@ particularly, when counting things. By way of example, suppose you have a model like this:: + from django.db import models + class Item(models.Model): name = models.CharField(max_length=10) data = models.IntegerField() @@ -457,5 +464,6 @@ For example, if you wanted to calculate the average number of authors per book you first annotate the set of books with the author count, then aggregate that author count, referencing the annotation field:: + >>> from django.db.models import Count, Avg >>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors')) {'num_authors__avg': 1.66} diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index 2a0f7e4ce0..b940b09d33 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -62,6 +62,8 @@ For example, this custom ``Manager`` offers a method ``with_counts()``, which returns a list of all ``OpinionPoll`` objects, each with an extra ``num_responses`` attribute that is the result of an aggregate query:: + from django.db import models + class PollManager(models.Manager): def with_counts(self): from django.db import connection @@ -101,6 +103,8 @@ Modifying initial Manager QuerySets A ``Manager``'s base ``QuerySet`` returns all objects in the system. For example, using this model:: + from django.db import models + class Book(models.Model): title = models.CharField(max_length=100) author = models.CharField(max_length=50) @@ -236,7 +240,7 @@ class, but still customize the default manager. For example, suppose you have this base class:: class AbstractBase(models.Model): - ... + # ... objects = CustomManager() class Meta: @@ -246,14 +250,15 @@ If you use this directly in a subclass, ``objects`` will be the default manager if you declare no managers in the base class:: class ChildA(AbstractBase): - ... + # ... # This class has CustomManager as the default manager. + pass If you want to inherit from ``AbstractBase``, but provide a different default manager, you can provide the default manager on the child class:: class ChildB(AbstractBase): - ... + # ... # An explicit default manager. default_manager = OtherManager() @@ -274,9 +279,10 @@ it into the inheritance hierarchy *after* the defaults:: abstract = True class ChildC(AbstractBase, ExtraManager): - ... + # ... # Default manager is CustomManager, but OtherManager is # also available via the "extra_manager" attribute. + pass Note that while you can *define* a custom manager on the abstract model, you can't *invoke* any methods using the abstract model. That is:: @@ -349,8 +355,7 @@ the manager class:: class MyManager(models.Manager): use_for_related_fields = True - - ... + # ... If this attribute is set on the *default* manager for a model (only the default manager is considered in these situations), Django will use that class @@ -396,7 +401,8 @@ it, whereas the following will not work:: # BAD: Incorrect code class MyManager(models.Manager): - ... + # ... + pass # Sets the attribute on an instance of MyManager. Django will # ignore this setting. @@ -404,7 +410,7 @@ it, whereas the following will not work:: mgr.use_for_related_fields = True class MyModel(models.Model): - ... + # ... objects = mgr # End of incorrect code. diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index dd7714052d..c0ba53ddd7 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -90,6 +90,8 @@ attributes. Be careful not to choose field names that conflict with the Example:: + from django.db import models + class Musician(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -180,7 +182,7 @@ ones: ('L', 'Large'), ) name = models.CharField(max_length=60) - shirt_size = models.CharField(max_length=2, choices=SHIRT_SIZES) + shirt_size = models.CharField(max_length=1, choices=SHIRT_SIZES) :: @@ -290,8 +292,11 @@ For example, if a ``Car`` model has a ``Manufacturer`` -- that is, a ``Manufacturer`` makes multiple cars but each ``Car`` only has one ``Manufacturer`` -- use the following definitions:: + from django.db import models + class Manufacturer(models.Model): # ... + pass class Car(models.Model): manufacturer = models.ForeignKey(Manufacturer) @@ -340,8 +345,11 @@ For example, if a ``Pizza`` has multiple ``Topping`` objects -- that is, a ``Topping`` can be on multiple pizzas and each ``Pizza`` has multiple toppings -- here's how you'd represent that:: + from django.db import models + class Topping(models.Model): # ... + pass class Pizza(models.Model): # ... @@ -403,6 +411,8 @@ intermediate model. The intermediate model is associated with the that will act as an intermediary. For our musician example, the code would look something like this:: + from django.db import models + class Person(models.Model): name = models.CharField(max_length=128) @@ -583,6 +593,7 @@ It's perfectly OK to relate a model to one from another app. To do this, import the related model at the top of the file where your model is defined. Then, just refer to the other model class wherever needed. For example:: + from django.db import models from geography.models import ZipCode class Restaurant(models.Model): @@ -630,6 +641,8 @@ Meta options Give your model metadata by using an inner ``class Meta``, like so:: + from django.db import models + class Ox(models.Model): horn_length = models.IntegerField() @@ -660,6 +673,8 @@ model. For example, this model has a few custom methods:: + from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) @@ -729,6 +744,8 @@ A classic use-case for overriding the built-in methods is if you want something to happen whenever you save an object. For example (see :meth:`~Model.save` for documentation of the parameters it accepts):: + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() @@ -740,6 +757,8 @@ to happen whenever you save an object. For example (see You can also prevent saving:: + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() @@ -826,6 +845,8 @@ the child (and Django will raise an exception). An example:: + from django.db import models + class CommonInfo(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() @@ -854,14 +875,16 @@ attribute. If a child class does not declare its own :ref:`Meta <meta-options>` class, it will inherit the parent's :ref:`Meta <meta-options>`. If the child wants to extend the parent's :ref:`Meta <meta-options>` class, it can subclass it. For example:: + from django.db import models + class CommonInfo(models.Model): - ... + # ... class Meta: abstract = True ordering = ['name'] class Student(CommonInfo): - ... + # ... class Meta(CommonInfo.Meta): db_table = 'student_info' @@ -901,6 +924,8 @@ abstract base class (only), part of the name should contain For example, given an app ``common/models.py``:: + from django.db import models + class Base(models.Model): m2m = models.ManyToManyField(OtherModel, related_name="%(app_label)s_%(class)s_related") @@ -949,6 +974,8 @@ relationship introduces links between the child model and each of its parents (via an automatically-created :class:`~django.db.models.OneToOneField`). For example:: + from django.db import models + class Place(models.Model): name = models.CharField(max_length=50) address = models.CharField(max_length=80) @@ -998,7 +1025,7 @@ If the parent has an ordering and you don't want the child to have any natural ordering, you can explicitly disable it:: class ChildModel(ParentModel): - ... + # ... class Meta: # Remove parent's ordering effect ordering = [] @@ -1061,15 +1088,21 @@ Proxy models are declared like normal models. You tell Django that it's a proxy model by setting the :attr:`~django.db.models.Options.proxy` attribute of the ``Meta`` class to ``True``. -For example, suppose you want to add a method to the ``Person`` model described -above. You can do it like this:: +For example, suppose you want to add a method to the ``Person`` model. You can do it like this:: + + from django.db import models + + class Person(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) class MyPerson(Person): class Meta: proxy = True def do_something(self): - ... + # ... + pass The ``MyPerson`` class operates on the same database table as its parent ``Person`` class. In particular, any new instances of ``Person`` will also be @@ -1125,8 +1158,11 @@ classes will still be available. Continuing our example from above, you could change the default manager used when you query the ``Person`` model like this:: + from django.db import models + class NewManager(models.Manager): - ... + # ... + pass class MyPerson(Person): objects = NewManager() diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 2553eac27a..bdbdd3fa2a 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -17,6 +17,8 @@ models, which comprise a Weblog application: .. code-block:: python + from django.db import models + class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() @@ -100,7 +102,8 @@ Saving ``ForeignKey`` and ``ManyToManyField`` fields Updating a :class:`~django.db.models.ForeignKey` field works exactly the same way as saving a normal field -- simply assign an object of the right type to the field in question. This example updates the ``blog`` attribute of an -``Entry`` instance ``entry``:: +``Entry`` instance ``entry``, assuming appropriate instances of ``Entry`` and +``Blog`` are already saved to the database (so we can retrieve them below):: >>> from blog.models import Entry >>> entry = Entry.objects.get(pk=1) @@ -711,9 +714,9 @@ for you transparently. Caching and QuerySets --------------------- -Each :class:`~django.db.models.query.QuerySet` contains a cache, to minimize -database access. It's important to understand how it works, in order to write -the most efficient code. +Each :class:`~django.db.models.query.QuerySet` contains a cache to minimize +database access. Understanding how it works will allow you to write the most +efficient code. In a newly created :class:`~django.db.models.query.QuerySet`, the cache is empty. The first time a :class:`~django.db.models.query.QuerySet` is evaluated @@ -744,6 +747,43 @@ To avoid this problem, simply save the >>> print([p.headline for p in queryset]) # Evaluate the query set. >>> print([p.pub_date for p in queryset]) # Re-use the cache from the evaluation. +When querysets are not cached +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Querysets do not always cache their results. When evaluating only *part* of +the queryset, the cache is checked, but if it is not populated then the items +returned by the subsequent query are not cached. Specifically, this means that +:ref:`limiting the queryset <limiting-querysets>` using an array slice or an +index will not populate the cache. + +For example, repeatedly getting a certain index in a queryset object will query +the database each time:: + + >>> queryset = Entry.objects.all() + >>> print queryset[5] # Queries the database + >>> print queryset[5] # Queries the database again + +However, if the entire queryset has already been evaluated, the cache will be +checked instead:: + + >>> queryset = Entry.objects.all() + >>> [entry for entry in queryset] # Queries the database + >>> print queryset[5] # Uses cache + >>> print queryset[5] # Uses cache + +Here are some examples of other actions that will result in the entire queryset +being evaluated and therefore populate the cache:: + + >>> [entry for entry in queryset] + >>> bool(queryset) + >>> entry in queryset + >>> list(queryset) + +.. note:: + + Simply printing the queryset will not populate the cache. This is because + the call to ``__repr__()`` only returns a slice of the entire queryset. + .. _complex-lookups-with-q: Complex lookups with Q objects diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 78786996cd..e9a626f56b 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -45,14 +45,6 @@ You may perfom partial commits and rollbacks in your view code, typically with the :func:`atomic` context manager. However, at the end of the view, either all the changes will be committed, or none of them. -To disable this behavior for a specific view, you must set the -``transactions_per_request`` attribute of the view function itself to -``False``, like this:: - - def my_view(request): - do_stuff() - my_view.transactions_per_request = False - .. warning:: While the simplicity of this transaction model is appealing, it also makes it @@ -78,6 +70,26 @@ Note that only the execution of your view is enclosed in the transactions. Middleware runs outside of the transaction, and so does the rendering of template responses. +When :setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>` is enabled, it's +still possible to prevent views from running in a transaction. + +.. function:: non_atomic_requests(using=None) + + This decorator will negate the effect of :setting:`ATOMIC_REQUESTS + <DATABASE-ATOMIC_REQUESTS>` for a given view:: + + from django.db import transaction + + @transaction.non_atomic_requests + def my_view(request): + do_stuff() + + @transaction.non_atomic_requests(using='other') + def my_other_view(request): + do_stuff_on_the_other_database() + + It only works if it's applied to the view itself. + .. versionchanged:: 1.6 Django used to provide this feature via ``TransactionMiddleware``, which is @@ -519,8 +531,8 @@ Transaction states ------------------ The three functions described above relied on a concept called "transaction -states". This mechanisme was deprecated in Django 1.6, but it's still -available until Django 1.8. +states". This mechanism was deprecated in Django 1.6, but it's still available +until Django 1.8. At any time, each database connection is in one of these two states: @@ -552,25 +564,16 @@ API changes Transaction middleware ~~~~~~~~~~~~~~~~~~~~~~ -In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced +In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced by :setting:`ATOMIC_REQUESTS <DATABASE-ATOMIC_REQUESTS>`. While the general -behavior is the same, there are a few differences. +behavior is the same, there are two differences. -With the transaction middleware, it was still possible to switch to autocommit -or to commit explicitly in a view. Since :func:`atomic` guarantees atomicity, -this isn't allowed any longer. - -To avoid wrapping a particular view in a transaction, instead of:: - - @transaction.autocommit - def my_view(request): - do_stuff() - -you must now use this pattern:: - - def my_view(request): - do_stuff() - my_view.transactions_per_request = False +With the previous API, it was possible to switch to autocommit or to commit +explicitly anywhere inside a view. Since :setting:`ATOMIC_REQUESTS +<DATABASE-ATOMIC_REQUESTS>` relies on :func:`atomic` which enforces atomicity, +this isn't allowed any longer. However, at the toplevel, it's still possible +to avoid wrapping an entire view in a transaction. To achieve this, decorate +the view with :func:`non_atomic_requests` instead of :func:`autocommit`. The transaction middleware applied not only to view functions, but also to middleware modules that came after it. For instance, if you used the session @@ -624,6 +627,9 @@ you should now use:: finally: transaction.set_autocommit(False) +Unless you're implementing a transaction management framework, you shouldn't +ever need to do this. + Disabling transaction management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -653,7 +659,7 @@ Sequences of custom SQL queries If you're executing several :ref:`custom SQL queries <executing-custom-sql>` in a row, each one now runs in its own transaction, instead of sharing the same "automatic transaction". If you need to enforce atomicity, you must wrap -the sequence of queries in :func:`commit_on_success`. +the sequence of queries in :func:`atomic`. To check for this problem, look for calls to ``cursor.execute()``. They're usually followed by a call to ``transaction.commit_unless_managed()``, which diff --git a/docs/topics/files.txt b/docs/topics/files.txt index fb3cdd4af9..492e6a20b5 100644 --- a/docs/topics/files.txt +++ b/docs/topics/files.txt @@ -27,6 +27,8 @@ to deal with that file. Consider the following model, using an :class:`~django.db.models.ImageField` to store a photo:: + from django.db import models + class Car(models.Model): name = models.CharField(max_length=255) price = models.DecimalField(max_digits=5, decimal_places=2) diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 9d77cd5274..8745e9761d 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -56,6 +56,9 @@ telling the formset how many additional forms to show in addition to the number of forms it generates from the initial data. Lets take a look at an example:: + >>> import datetime + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, extra=2) >>> formset = ArticleFormSet(initial=[ ... {'title': u'Django is now open source', @@ -88,6 +91,8 @@ The ``max_num`` parameter to :func:`~django.forms.formsets.formset_factory` gives you the ability to limit the maximum number of empty forms the formset will display:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, extra=2, max_num=1) >>> formset = ArticleFormSet() >>> for form in formset: @@ -124,6 +129,8 @@ Validation with a formset is almost identical to a regular ``Form``. There is an ``is_valid`` method on the formset to provide a convenient way to validate all forms in the formset:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm) >>> data = { ... 'form-TOTAL_FORMS': u'1', @@ -230,6 +237,8 @@ A formset has a ``clean`` method similar to the one on a ``Form`` class. This is where you define your own validation that works at the formset level:: >>> from django.forms.formsets import BaseFormSet + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> class BaseArticleFormSet(BaseFormSet): ... def clean(self): @@ -274,8 +283,11 @@ Validating the number of forms in a formset If ``validate_max=True`` is passed to :func:`~django.forms.formsets.formset_factory`, validation will also check -that the number of forms in the data set is less than or equal to ``max_num``. +that the number of forms in the data set, minus those marked for +deletion, is less than or equal to ``max_num``. + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, max_num=1, validate_max=True) >>> data = { ... 'form-TOTAL_FORMS': u'2', @@ -329,6 +341,8 @@ Default: ``False`` Lets you create a formset with the ability to order:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, can_order=True) >>> formset = ArticleFormSet(initial=[ ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, @@ -385,6 +399,8 @@ Default: ``False`` Lets you create a formset with the ability to delete:: + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> ArticleFormSet = formset_factory(ArticleForm, can_delete=True) >>> formset = ArticleFormSet(initial=[ ... {'title': u'Article #1', 'pub_date': datetime.date(2008, 5, 10)}, @@ -437,6 +453,9 @@ accomplished. The formset base class provides an ``add_fields`` method. You can simply override this method to add your own fields or even redefine the default fields/attributes of the order and deletion fields:: + >>> from django.forms.formsets import BaseFormSet + >>> from django.forms.formsets import formset_factory + >>> from myapp.forms import ArticleForm >>> class BaseArticleFormSet(BaseFormSet): ... def add_fields(self, form, index): ... super(BaseArticleFormSet, self).add_fields(form, index) @@ -459,6 +478,10 @@ management form inside the template. Let's look at a sample view: .. code-block:: python + from django.forms.formsets import formset_factory + from django.shortcuts import render_to_response + from myapp.forms import ArticleForm + def manage_articles(request): ArticleFormSet = formset_factory(ArticleForm) if request.method == 'POST': @@ -534,6 +557,10 @@ a look at how this might be accomplished: .. code-block:: python + from django.forms.formsets import formset_factory + from django.shortcuts import render_to_response + from myapp.forms import ArticleForm, BookForm + def manage_articles(request): ArticleFormSet = formset_factory(ArticleForm) BookFormSet = formset_factory(BookForm) diff --git a/docs/topics/forms/media.txt b/docs/topics/forms/media.txt index c0d63bb8cf..b014e97119 100644 --- a/docs/topics/forms/media.txt +++ b/docs/topics/forms/media.txt @@ -49,6 +49,8 @@ define the media requirements. Here's a simple example:: + from django import froms + class CalendarWidget(forms.TextInput): class Media: css = { @@ -211,6 +213,7 @@ to using :setting:`MEDIA_URL`. For example, if the :setting:`MEDIA_URL` for your site was ``'http://uploads.example.com/'`` and :setting:`STATIC_URL` was ``None``:: + >>> from django import forms >>> class CalendarWidget(forms.TextInput): ... class Media: ... css = { @@ -267,6 +270,7 @@ Combining media objects Media objects can also be added together. When two media objects are added, the resulting Media object contains the union of the media from both files:: + >>> from django import forms >>> class CalendarWidget(forms.TextInput): ... class Media: ... css = { @@ -298,6 +302,7 @@ Regardless of whether you define a media declaration, *all* Form objects have a media property. The default value for this property is the result of adding the media definitions for all widgets that are part of the form:: + >>> from django import forms >>> class ContactForm(forms.Form): ... date = DateField(widget=CalendarWidget) ... name = CharField(max_length=40, widget=OtherWidget) diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index e58dade736..4c46c6c0c0 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -23,6 +23,7 @@ class from a Django model. For example:: >>> from django.forms import ModelForm + >>> from myapp.models import Article # Create the form class. >>> class ArticleForm(ModelForm): @@ -74,7 +75,7 @@ Model field Form field ``FileField`` ``FileField`` -``FilePathField`` ``CharField`` +``FilePathField`` ``FilePathField`` ``FloatField`` ``FloatField`` @@ -222,6 +223,9 @@ supplied, ``save()`` will update that instance. If it's not supplied, .. code-block:: python + >>> from myapp.models import Article + >>> from myapp.forms import ArticleForm + # Create a form instance from POST data. >>> f = ArticleForm(request.POST) @@ -316,6 +320,8 @@ these security concerns do not apply to you: 1. Set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used. For example:: + from django.forms import ModelForm + class AuthorForm(ModelForm): class Meta: model = Author @@ -401,6 +407,7 @@ of its default ``<input type="text">``, you can override the field's widget:: from django.forms import ModelForm, Textarea + from myapp.models import Author class AuthorForm(ModelForm): class Meta: @@ -421,6 +428,9 @@ you can do this by declaratively specifying fields like you would in a regular For example, if you wanted to use ``MyDateFormField`` for the ``pub_date`` field, you could do the following:: + from django.forms import ModelForm + from myapp.models import Article + class ArticleForm(ModelForm): pub_date = MyDateFormField() @@ -432,6 +442,9 @@ field, you could do the following:: If you want to override a field's default label, then specify the ``label`` parameter when declaring the form field:: + from django.forms import ModelForm, DateField + from myapp.models import Article + class ArticleForm(ModelForm): pub_date = DateField(label='Publication date') @@ -474,6 +487,26 @@ parameter when declaring the form field:: See the :doc:`form field documentation </ref/forms/fields>` for more information on fields and their arguments. + +Enabling localization of fields +------------------------------- + +.. versionadded:: 1.6 + +By default, the fields in a ``ModelForm`` will not localize their data. To +enable localization for fields, you can use the ``localized_fields`` +attribute on the ``Meta`` class. + + >>> from django.forms import ModelForm + >>> from myapp.models import Author + >>> class AuthorForm(ModelForm): + ... class Meta: + ... model = Author + ... localized_fields = ('birth_date',) + +If ``localized_fields`` is set to the special value ``'__all__'``, all fields +will be localized. + .. _overriding-modelform-clean-method: Overriding the clean() method @@ -556,6 +589,7 @@ definition. This may be more convenient if you do not have many customizations to make:: >>> from django.forms.models import modelform_factory + >>> from myapp.models import Book >>> BookForm = modelform_factory(Book, fields=("author", "title")) This can also be used to make simple modifications to existing forms, for @@ -570,6 +604,10 @@ keyword arguments, or the corresponding attributes on the ``ModelForm`` inner ``Meta`` class. Please see the ``ModelForm`` :ref:`modelforms-selecting-fields` documentation. +... or enable localization for specific fields:: + + >>> Form = modelform_factory(Author, form=AuthorForm, localized_fields=("birth_date",)) + .. _model-formsets: Model formsets @@ -582,6 +620,7 @@ of enhanced formset classes that make it easy to work with Django models. Let's reuse the ``Author`` model from above:: >>> from django.forms.models import modelformset_factory + >>> from myapp.models import Author >>> AuthorFormSet = modelformset_factory(Author) This will create a formset that is capable of working with the data associated @@ -620,6 +659,7 @@ Alternatively, you can create a subclass that sets ``self.queryset`` in ``__init__``:: from django.forms.models import BaseModelFormSet + from myapp.models import Author class BaseAuthorFormSet(BaseModelFormSet): def __init__(self, *args, **kwargs): @@ -663,6 +703,20 @@ class of a ``ModelForm`` works:: >>> AuthorFormSet = modelformset_factory( ... Author, widgets={'name': Textarea(attrs={'cols': 80, 'rows': 20}) +Enabling localization for fields with ``localized_fields`` +---------------------------------------------------------- + +.. versionadded:: 1.6 + +Using the ``localized_fields`` parameter, you can enable localization for +fields in the form. + + >>> AuthorFormSet = modelformset_factory( + ... Author, localized_fields=('value',)) + +If ``localized_fields`` is set to the special value ``'__all__'``, all fields +will be localized. + Providing initial values ------------------------ @@ -751,6 +805,10 @@ Using a model formset in a view Model formsets are very similar to formsets. Let's say we want to present a formset to edit ``Author`` model instances:: + from django.forms.models import modelformset_factory + from django.shortcuts import render_to_response + from myapp.models import Author + def manage_authors(request): AuthorFormSet = modelformset_factory(Author) if request.method == 'POST': @@ -779,12 +837,15 @@ the unique constraints on your model (either ``unique``, ``unique_together`` or on a ``model_formset`` and maintain this validation, you must call the parent class's ``clean`` method:: + from django.forms.models import BaseModelFormSet + class MyModelFormSet(BaseModelFormSet): def clean(self): super(MyModelFormSet, self).clean() # example custom validation across forms in the formset: for form in self.forms: # your custom formset validation + pass Using a custom queryset ----------------------- @@ -792,6 +853,10 @@ Using a custom queryset As stated earlier, you can override the default queryset used by the model formset:: + from django.forms.models import modelformset_factory + from django.shortcuts import render_to_response + from myapp.models import Author + def manage_authors(request): AuthorFormSet = modelformset_factory(Author) if request.method == "POST": @@ -878,6 +943,8 @@ Inline formsets is a small abstraction layer on top of model formsets. These simplify the case of working with related objects via a foreign key. Suppose you have these two models:: + from django.db import models + class Author(models.Model): name = models.CharField(max_length=100) diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index 80bd5f3c44..54d748d961 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -15,6 +15,7 @@ Basic file uploads Consider a simple form containing a :class:`~django.forms.FileField`:: + # In forms.py... from django import forms class UploadFileForm(forms.Form): @@ -39,6 +40,7 @@ something like:: from django.http import HttpResponseRedirect from django.shortcuts import render_to_response + from .forms import UploadFileForm # Imaginary function to handle an uploaded file. from somewhere import handle_uploaded_file diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index acad61eb2a..772ee122d5 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -118,6 +118,13 @@ To use cookies-based sessions, set the :setting:`SESSION_ENGINE` setting to stored using Django's tools for :doc:`cryptographic signing </topics/signing>` and the :setting:`SECRET_KEY` setting. +.. note:: + + When using cookies-based sessions :mod:`django.contrib.sessions` can be + removed from :setting:`INSTALLED_APPS` setting because data is loaded + from the key itself and not from the database, so there is no need for the + creation and usage of ``django.contrib.sessions.models.Session`` table. + .. note:: It's recommended to leave the :setting:`SESSION_COOKIE_HTTPONLY` setting @@ -125,6 +132,17 @@ and the :setting:`SECRET_KEY` setting. .. warning:: + **If the SECRET_KEY is not kept secret, this can lead to arbitrary remote + code execution.** + + An attacker in possession of the :setting:`SECRET_KEY` can not only + generate falsified session data, which your site will trust, but also + remotely execute arbitrary code, as the data is serialized using pickle. + + If you use cookie-based sessions, pay extra care that your secret key is + always kept completely secret, for any system which might be remotely + accessible. + **The session data is signed but not encrypted** When using the cookies backend the session data can be read by the client. diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 9a96199dba..8a3f240307 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -123,6 +123,8 @@ is ``(?P<name>pattern)``, where ``name`` is the name of the group and Here's the above example URLconf, rewritten to use named groups:: + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^articles/2003/$', 'news.views.special_case_2003'), url(r'^articles/(?P<year>\d{4})/$', 'news.views.year_archive'), @@ -192,6 +194,8 @@ A convenient trick is to specify default parameters for your views' arguments. Here's an example URLconf and view:: # URLconf + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^blog/$', 'blog.views.page'), url(r'^blog/page(?P<num>\d+)/$', 'blog.views.page'), @@ -370,11 +374,15 @@ An included URLconf receives any captured parameters from parent URLconfs, so the following example is valid:: # In settings/urls/main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^(?P<username>\w+)/blog/', include('foo.urls.blog')), ) # In foo/urls/blog.py + from django.conf.urls import patterns, url + urlpatterns = patterns('foo.views', url(r'^$', 'blog.index'), url(r'^archive/$', 'blog.archive'), @@ -397,6 +405,8 @@ function. For example:: + from django.conf.urls import patterns, url + urlpatterns = patterns('blog.views', url(r'^blog/(?P<year>\d{4})/$', 'year_archive', {'foo': 'bar'}), ) @@ -427,11 +437,15 @@ For example, these two URLconf sets are functionally identical: Set one:: # main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^blog/', include('inner'), {'blogid': 3}), ) # inner.py + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive'), url(r'^about/$', 'mysite.views.about'), @@ -440,11 +454,15 @@ Set one:: Set two:: # main.py + from django.conf.urls import include, patterns, url + urlpatterns = patterns('', url(r'^blog/', include('inner')), ) # inner.py + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive', {'blogid': 3}), url(r'^about/$', 'mysite.views.about', {'blogid': 3}), @@ -464,6 +482,8 @@ supported -- you can pass any callable object as the view. For example, given this URLconf in "string" notation:: + from django.conf.urls import patterns, url + urlpatterns = patterns('', url(r'^archive/$', 'mysite.views.archive'), url(r'^about/$', 'mysite.views.about'), @@ -473,6 +493,7 @@ For example, given this URLconf in "string" notation:: You can accomplish the same thing by passing objects rather than strings. Just be sure to import the objects:: + from django.conf.urls import patterns, url from mysite.views import archive, about, contact urlpatterns = patterns('', @@ -485,6 +506,7 @@ The following example is functionally identical. It's just a bit more compact because it imports the module that contains the views, rather than importing each view individually:: + from django.conf.urls import patterns, url from mysite import views urlpatterns = patterns('', @@ -501,6 +523,7 @@ the view prefix (as explained in "The view prefix" above) will have no effect. Note that :doc:`class based views</topics/class-based-views/index>` must be imported:: + from django.conf.urls import patterns, url from mysite.views import ClassBasedView urlpatterns = patterns('', @@ -612,6 +635,9 @@ It's fairly common to use the same view function in multiple URL patterns in your URLconf. For example, these two URL patterns both point to the ``archive`` view:: + from django.conf.urls import patterns, url + from mysite.views import archive + urlpatterns = patterns('', url(r'^archive/(\d{4})/$', archive), url(r'^archive-summary/(\d{4})/$', archive, {'summary': True}), @@ -630,6 +656,9 @@ matching. Here's the above example, rewritten to use named URL patterns:: + from django.conf.urls import patterns, url + from mysite.views import archive + urlpatterns = patterns('', url(r'^archive/(\d{4})/$', archive, name="full-archive"), url(r'^archive-summary/(\d{4})/$', archive, {'summary': True}, name="arch-summary"), @@ -803,6 +832,8 @@ However, you can also ``include()`` a 3-tuple containing:: For example:: + from django.conf.urls import include, patterns, url + help_patterns = patterns('', url(r'^basic/$', 'apps.help.views.views.basic'), url(r'^advanced/$', 'apps.help.views.views.advanced'), diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index f73ec4f5be..5c27c9c958 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -70,6 +70,8 @@ documentation. Just return an instance of one of those subclasses instead of a normal :class:`~django.http.HttpResponse` in order to signify an error. For example:: + from django.http import HttpResponse, HttpResponseNotFound + def my_view(request): # ... if foo: @@ -83,6 +85,8 @@ the :class:`~django.http.HttpResponse` documentation, you can also pass the HTTP status code into the constructor for :class:`~django.http.HttpResponse` to create a return class for any status code you like. For example:: + from django.http import HttpResponse + def my_view(request): # ... @@ -110,6 +114,8 @@ standard error page for your application, along with an HTTP error code 404. Example usage:: from django.http import Http404 + from django.shortcuts import render_to_response + from polls.models import Poll def detail(request, poll_id): try: @@ -225,3 +231,25 @@ same way you can for the 404 and 500 views by specifying a ``handler403`` in your URLconf:: handler403 = 'mysite.views.my_custom_permission_denied_view' + +.. _http_bad_request_view: + +The 400 (bad request) view +-------------------------- + +When a :exc:`~django.core.exceptions.SuspiciousOperation` is raised in Django, +the it may be handled by a component of Django (for example resetting the +session data). If not specifically handled, Django will consider the current +request a 'bad request' instead of a server error. + +The view ``django.views.defaults.bad_request``, is otherwise very similar to +the ``server_error`` view, but returns with the status code 400 indicating that +the error condition was the result of a client operation. + +Like the ``server_error`` view, the default ``bad_request`` should suffice for +99% of Web applications, but if you want to override the view, you can specify +``handler400`` in your URLconf, like so:: + + handler400 = 'mysite.views.my_custom_bad_request_view' + +``bad_request`` views are also only used when :setting:`DEBUG` is ``False``. diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index e4a043b08f..5ed60d0a94 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -54,6 +54,8 @@ FAQ <time-zones-faq>`. Concepts ======== +.. _naive_vs_aware_datetimes: + Naive and aware datetime objects -------------------------------- diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 5b4ffea528..ce6697908f 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -80,6 +80,7 @@ In this example, the text ``"Welcome to my site."`` is marked as a translation string:: from django.utils.translation import ugettext as _ + from django.http import HttpResponse def my_view(request): output = _("Welcome to my site.") @@ -89,6 +90,7 @@ Obviously, you could code this without using the alias. This example is identical to the previous one:: from django.utils.translation import ugettext + from django.http import HttpResponse def my_view(request): output = ugettext("Welcome to my site.") @@ -192,6 +194,7 @@ of its value.) For example:: from django.utils.translation import ungettext + from django.http import HttpResponse def hello_world(request, count): page = ungettext( @@ -208,6 +211,7 @@ languages as the ``count`` variable. Lets see a slightly more complex usage example:: from django.utils.translation import ungettext + from myapp.models import Report count = Report.objects.count() if count == 1: @@ -283,6 +287,7 @@ For example:: or:: + from django.db import models from django.utils.translation import pgettext_lazy class MyThing(models.Model): @@ -328,6 +333,7 @@ Model fields and relationships ``verbose_name`` and ``help_text`` option values For example, to translate the help text of the *name* field in the following model, do the following:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -336,8 +342,6 @@ model, do the following:: You can mark names of ``ForeignKey``, ``ManyTomanyField`` or ``OneToOneField`` relationship as translatable by using their ``verbose_name`` options:: - from django.utils.translation import ugettext_lazy as _ - class MyThing(models.Model): kind = models.ForeignKey(ThingKind, related_name='kinds', verbose_name=_('kind')) @@ -355,6 +359,7 @@ It is recommended to always provide explicit relying on the fallback English-centric and somewhat naïve determination of verbose names Django performs by looking at the model's class name:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -370,6 +375,7 @@ Model methods ``short_description`` attribute values For model methods, you can provide translations to Django and the admin site with the ``short_description`` attribute:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -404,6 +410,7 @@ If you ever see output that looks like ``"hello If you don't like the long ``ugettext_lazy`` name, you can just alias it as ``_`` (underscore), like so:: + from django.db import models from django.utils.translation import ugettext_lazy as _ class MyThing(models.Model): @@ -429,6 +436,9 @@ definition. Therefore, you are authorized to pass a key name instead of an integer as the ``number`` argument. Then ``number`` will be looked up in the dictionary under that key during string interpolation. Here's example:: + from django import forms + from django.utils.translation import ugettext_lazy + class MyForm(forms.Form): error_message = ungettext_lazy("You only provided %(num)d argument", "You only provided %(num)d arguments", 'num') @@ -461,6 +471,7 @@ that concatenates its contents *and* converts them to strings only when the result is included in a string. For example:: from django.utils.translation import string_concat + from django.utils.translation import ugettext_lazy ... name = ugettext_lazy('John Lennon') instrument = ugettext_lazy('guitar') @@ -687,7 +698,7 @@ or with the ``{#`` ... ``#}`` :ref:`one-line comment constructs <template-commen .. code-block:: html+django - {# Translators: Label of a button that triggers search{% endcomment #} + {# Translators: Label of a button that triggers search #} <button type="submit">{% trans "Go" %}</button> {# Translators: This is a text of the base template #} @@ -722,6 +733,31 @@ or with the ``{#`` ... ``#}`` :ref:`one-line comment constructs <template-commen msgid "Ambiguous translatable block of text" msgstr "" +.. templatetag:: language + +Switching language in templates +------------------------------- + +If you want to select a language within a template, you can use the +``language`` template tag: + +.. code-block:: html+django + + {% load i18n %} + + {% get_current_language as LANGUAGE_CODE %} + <!-- Current language: {{ LANGUAGE_CODE }} --> + <p>{% trans "Welcome to our page" %}</p> + + {% language 'en' %} + {% get_current_language as LANGUAGE_CODE %} + <!-- Current language: {{ LANGUAGE_CODE }} --> + <p>{% trans "Welcome to our page" %}</p> + {% endlanguage %} + +While the first occurrence of "Welcome to our page" uses the current language, +the second will always be in English. + .. _template-translation-vars: Other tags @@ -1126,13 +1162,11 @@ active language. Example:: .. _reversing_in_templates: -.. templatetag:: language - Reversing in templates ---------------------- If localized URLs get reversed in templates they always use the current -language. To link to a URL in another language use the ``language`` +language. To link to a URL in another language use the :ttag:`language` template tag. It enables the given language in the enclosed template section: .. code-block:: html+django @@ -1640,6 +1674,8 @@ preference available as ``request.LANGUAGE_CODE`` for each :class:`~django.http.HttpRequest`. Feel free to read this value in your view code. Here's a simple example:: + from django.http import HttpResponse + def hello_world(request, count): if request.LANGUAGE_CODE == 'de-at': return HttpResponse("You prefer to read Austrian German.") diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index a31dc01cc5..a88201ad47 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -394,7 +394,7 @@ requirements of logging in Web server environment. Loggers ------- -Django provides three built-in loggers. +Django provides four built-in loggers. ``django`` ~~~~~~~~~~ @@ -434,6 +434,35 @@ For performance reasons, SQL logging is only enabled when ``settings.DEBUG`` is set to ``True``, regardless of the logging level or handlers that are installed. +``django.security.*`` +~~~~~~~~~~~~~~~~~~~~~~ + +The security loggers will receive messages on any occurrence of +:exc:`~django.core.exceptions.SuspiciousOperation`. There is a sub-logger for +each sub-type of SuspiciousOperation. The level of the log event depends on +where the exception is handled. Most occurrences are logged as a warning, while +any ``SuspiciousOperation`` that reaches the WSGI handler will be logged as an +error. For example, when an HTTP ``Host`` header is included in a request from +a client that does not match :setting:`ALLOWED_HOSTS`, Django will return a 400 +response, and an error message will be logged to the +``django.security.DisallowedHost`` logger. + +Only the parent ``django.security`` logger is configured by default, and all +child loggers will propagate to the parent logger. The ``django.security`` +logger is configured the same as the ``django.request`` logger, and any error +events will be mailed to admins. Requests resulting in a 400 response due to +a ``SuspiciousOperation`` will not be logged to the ``django.request`` logger, +but only to the ``django.security`` logger. + +To silence a particular type of SuspiciousOperation, you can override that +specific logger following this example:: + + 'loggers': { + 'django.security.DisallowedHost': { + 'handlers': ['null'], + 'propagate': False, + }, + Handlers -------- diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index 22e609c75c..9a0438e9e5 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -201,8 +201,8 @@ According to :pep:`3333`: Specifically, :attr:`HttpResponse.content <django.http.HttpResponse.content>` contains ``bytes``, which may become an issue if you compare it with a ``str`` in your tests. The preferred solution is to rely on -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains`. These methods accept a +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains`. These methods accept a response and a unicode string as arguments. Coding guidelines diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index cefb770469..b7f49d2b97 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -113,7 +113,8 @@ two databases. Controlling creation order for test databases --------------------------------------------- -By default, Django will always create the ``default`` database first. +By default, Django will assume all databases depend on the ``default`` +database and therefore always create the ``default`` database first. However, no guarantees are made on the creation order of any other databases in your test setup. @@ -129,6 +130,7 @@ can specify the dependencies that exist using the }, 'diamonds': { # ... db settings + 'TEST_DEPENDENCIES': [] }, 'clubs': { # ... db settings diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index fc2b393898..2b1db5e501 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -21,17 +21,16 @@ module defines tests using a class-based approach. .. admonition:: unittest2 - Python 2.7 introduced some major changes to the unittest library, + Python 2.7 introduced some major changes to the ``unittest`` library, adding some extremely useful features. To ensure that every Django project can benefit from these new features, Django ships with a - copy of unittest2_, a copy of the Python 2.7 unittest library, - backported for Python 2.6 compatibility. + copy of unittest2_, a copy of Python 2.7's ``unittest``, backported for + Python 2.6 compatibility. To access this library, Django provides the ``django.utils.unittest`` module alias. If you are using Python - 2.7, or you have installed unittest2 locally, Django will map the - alias to the installed version of the unittest library. Otherwise, - Django will use its own bundled version of unittest2. + 2.7, or you have installed ``unittest2`` locally, Django will map the alias + to it. Otherwise, Django will use its own bundled version of ``unittest2``. To use this alias, simply use:: @@ -41,8 +40,8 @@ module defines tests using a class-based approach. import unittest - If you want to continue to use the base unittest library, you can -- - you just won't get any of the nice new unittest2 features. + If you want to continue to use the legacy ``unittest`` library, you can -- + you just won't get any of the nice new ``unittest2`` features. .. _unittest2: http://pypi.python.org/pypi/unittest2 @@ -697,7 +696,7 @@ Use the ``django.test.client.Client`` class to make requests. After you call this method, the test client will have all the cookies and session data cleared to defaults. Subsequent requests will appear - to come from an AnonymousUser. + to come from an :class:`~django.contrib.auth.models.AnonymousUser`. Testing responses ~~~~~~~~~~~~~~~~~ @@ -858,24 +857,46 @@ SimpleTestCase .. class:: SimpleTestCase() -A very thin subclass of :class:`unittest.TestCase`, it extends it with some -basic functionality like: +A thin subclass of :class:`unittest.TestCase`, it extends it with some basic +functionality like: * Saving and restoring the Python warning machinery state. -* Checking that a callable :meth:`raises a certain exception <SimpleTestCase.assertRaisesMessage>`. -* :meth:`Testing form field rendering <SimpleTestCase.assertFieldOutput>`. -* Testing server :ref:`HTML responses for the presence/lack of a given fragment <assertions>`. -* The ability to run tests with :ref:`modified settings <overriding-settings>` +* Some useful assertions like: + + * Checking that a callable :meth:`raises a certain exception + <SimpleTestCase.assertRaisesMessage>`. + * Testing form field :meth:`rendering and error treatment + <SimpleTestCase.assertFieldOutput>`. + * Testing :meth:`HTML responses for the presence/lack of a given fragment + <SimpleTestCase.assertContains>`. + * Verifying that a template :meth:`has/hasn't been used to generate a given + response content <SimpleTestCase.assertTemplateUsed>`. + * Verifying a HTTP :meth:`redirect <SimpleTestCase.assertRedirects>` is + performed by the app. + * Robustly testing two :meth:`HTML fragments <SimpleTestCase.assertHTMLEqual>` + for equality/inequality or :meth:`containment <SimpleTestCase.assertInHTML>`. + * Robustly testing two :meth:`XML fragments <SimpleTestCase.assertXMLEqual>` + for equality/inequality. + * Robustly testing two :meth:`JSON fragments <SimpleTestCase.assertJSONEqual>` + for equality. + +* The ability to run tests with :ref:`modified settings <overriding-settings>`. +* Using the :attr:`~SimpleTestCase.client` :class:`~django.test.client.Client`. +* Custom test-time :attr:`URL maps <SimpleTestCase.urls>`. + +.. versionchanged:: 1.6 + + The latter two features were moved from ``TransactionTestCase`` to + ``SimpleTestCase`` in Django 1.6. If you need any of the other more complex and heavyweight Django-specific features like: -* Using the :attr:`~TestCase.client` :class:`~django.test.client.Client`. * Testing or using the ORM. -* Database :attr:`~TestCase.fixtures`. -* Custom test-time :attr:`URL maps <TestCase.urls>`. +* Database :attr:`~TransactionTestCase.fixtures`. * Test :ref:`skipping based on database backend features <skipping-tests>`. -* The remaining specialized :ref:`assert* <assertions>` methods. +* The remaining specialized :meth:`assert* + <TransactionTestCase.assertQuerysetEqual>` methods. then you should use :class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase` instead. @@ -904,14 +925,23 @@ to test the effects of commit and rollback: * A ``TestCase``, on the other hand, does not truncate tables after a test. Instead, it encloses the test code in a database transaction that is rolled - back at the end of the test. It also prevents the code under test from - issuing any commit or rollback operations on the database, to ensure that the - rollback at the end of the test restores the database to its initial state. + back at the end of the test. Both explicit commits like + ``transaction.commit()`` and implicit ones that may be caused by + ``Model.save()`` are replaced with a ``nop`` operation. This guarantees that + the rollback at the end of the test restores the database to its initial + state. When running on a database that does not support rollback (e.g. MySQL with the MyISAM storage engine), ``TestCase`` falls back to initializing the database by truncating tables and reloading initial data. +.. warning:: + + While ``commit`` and ``rollback`` operations still *appear* to work when + used in ``TestCase``, no actual commit or rollback will be performed by the + database. This can cause your tests to pass or fail unexpectedly. Always + use ``TransactionalTestCase`` when testing transactional behavior. + .. note:: .. versionchanged:: 1.5 @@ -923,7 +953,7 @@ to test the effects of commit and rollback: key values started at one in :class:`~django.test.TransactionTestCase` tests. - Tests should not depend on this behaviour, but for legacy tests that do, the + Tests should not depend on this behavior, but for legacy tests that do, the :attr:`~TransactionTestCase.reset_sequences` attribute can be used until the test has been properly updated. @@ -1137,9 +1167,9 @@ Test cases features Default test client ~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client +.. attribute:: SimpleTestCase.client -Every test case in a ``django.test.TestCase`` instance has access to an +Every test case in a ``django.test.*TestCase`` instance has access to an instance of a Django test client. This client can be accessed as ``self.client``. This client is recreated for each test, so you don't have to worry about state (such as cookies) carrying over from one test to another. @@ -1176,10 +1206,10 @@ This means, instead of instantiating a ``Client`` in each test:: Customizing the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client_class +.. attribute:: SimpleTestCase.client_class If you want to use a different ``Client`` class (for example, a subclass -with customized behavior), use the :attr:`~TestCase.client_class` class +with customized behavior), use the :attr:`~SimpleTestCase.client_class` class attribute:: from django.test import TestCase @@ -1200,11 +1230,12 @@ attribute:: Fixture loading ~~~~~~~~~~~~~~~ -.. attribute:: TestCase.fixtures +.. attribute:: TransactionTestCase.fixtures A test case for a database-backed Web site isn't much use if there isn't any data in the database. To make it easy to put test data into the database, -Django's custom ``TestCase`` class provides a way of loading **fixtures**. +Django's custom ``TransactionTestCase`` class provides a way of loading +**fixtures**. A fixture is a collection of data that Django knows how to import into a database. For example, if your site has user accounts, you might set up a @@ -1273,7 +1304,7 @@ or by the order of test execution. URLconf configuration ~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.urls +.. attribute:: SimpleTestCase.urls If your application provides views, you may want to include tests that use the test client to exercise those views. However, an end user is free to deploy the @@ -1282,9 +1313,9 @@ tests can't rely upon the fact that your views will be available at a particular URL. In order to provide a reliable URL space for your test, -``django.test.TestCase`` provides the ability to customize the URLconf +``django.test.*TestCase`` classes provide the ability to customize the URLconf configuration for the duration of the execution of a test suite. If your -``TestCase`` instance defines an ``urls`` attribute, the ``TestCase`` will use +``*TestCase`` instance defines an ``urls`` attribute, the ``*TestCase`` will use the value of that attribute as the :setting:`ROOT_URLCONF` for the duration of that test. @@ -1307,7 +1338,7 @@ URLconf for the duration of the test case. Multi-database support ~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.multi_db +.. attribute:: TransactionTestCase.multi_db Django sets up a test database corresponding to every database that is defined in the :setting:`DATABASES` definition in your settings @@ -1340,12 +1371,12 @@ This test case will flush *all* the test databases before running Overriding settings ~~~~~~~~~~~~~~~~~~~ -.. method:: TestCase.settings +.. method:: SimpleTestCase.settings For testing purposes it's often useful to change a setting temporarily and revert to the original value after running the testing code. For this use case Django provides a standard Python context manager (see :pep:`343`) -:meth:`~django.test.TestCase.settings`, which can be used like this:: +:meth:`~django.test.SimpleTestCase.settings`, which can be used like this:: from django.test import TestCase @@ -1435,8 +1466,8 @@ MEDIA_ROOT, DEFAULT_FILE_STORAGE Default file storage Emptying the test outbox ~~~~~~~~~~~~~~~~~~~~~~~~ -If you use Django's custom ``TestCase`` class, the test runner will clear the -contents of the test email outbox at the start of each test case. +If you use any of Django's custom ``TestCase`` classes, the test runner will +clear thecontents of the test email outbox at the start of each test case. For more detail on email services during tests, see `Email services`_ below. @@ -1486,31 +1517,7 @@ your test suite. self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid email address.']}) - -.. method:: TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) - - Asserts that a ``Response`` instance produced the given ``status_code`` and - that ``text`` appears in the content of the response. If ``count`` is - provided, ``text`` must occur exactly ``count`` times in the response. - - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with - the response content will be based on HTML semantics instead of - character-by-character equality. Whitespace is ignored in most cases, - attribute ordering is not significant. See - :meth:`~SimpleTestCase.assertHTMLEqual` for more details. - -.. method:: TestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) - - Asserts that a ``Response`` instance produced the given ``status_code`` and - that ``text`` does not appears in the content of the response. - - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with - the response content will be based on HTML semantics instead of - character-by-character equality. Whitespace is ignored in most cases, - attribute ordering is not significant. See - :meth:`~SimpleTestCase.assertHTMLEqual` for more details. - -.. method:: TestCase.assertFormError(response, form, field, errors, msg_prefix='') +.. method:: SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='') Asserts that a field on a form raises the provided list of errors when rendered on the form. @@ -1525,7 +1532,51 @@ your test suite. ``errors`` is an error string, or a list of error strings, that are expected as a result of form validation. -.. method:: TestCase.assertTemplateUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertFormsetError(response, formset, form_index, field, errors, msg_prefix='') + + .. versionadded:: 1.6 + + Asserts that the ``formset`` raises the provided list of errors when + rendered. + + ``formset`` is the name the ``Formset`` instance was given in the template + context. + + ``form_index`` is the number of the form within the ``Formset``. If + ``form_index`` has a value of ``None``, non-form errors (errors you can + access via ``formset.non_form_errors()``) will be checked. + + ``field`` is the name of the field on the form to check. If ``field`` + has a value of ``None``, non-field errors (errors you can access via + ``form.non_field_errors()``) will be checked. + + ``errors`` is an error string, or a list of error strings, that are + expected as a result of form validation. + +.. method:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) + + Asserts that a ``Response`` instance produced the given ``status_code`` and + that ``text`` appears in the content of the response. If ``count`` is + provided, ``text`` must occur exactly ``count`` times in the response. + + Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with + the response content will be based on HTML semantics instead of + character-by-character equality. Whitespace is ignored in most cases, + attribute ordering is not significant. See + :meth:`~SimpleTestCase.assertHTMLEqual` for more details. + +.. method:: SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) + + Asserts that a ``Response`` instance produced the given ``status_code`` and + that ``text`` does not appears in the content of the response. + + Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with + the response content will be based on HTML semantics instead of + character-by-character equality. Whitespace is ignored in most cases, + attribute ordering is not significant. See + :meth:`~SimpleTestCase.assertHTMLEqual` for more details. + +.. method:: SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was used in rendering the response. @@ -1539,15 +1590,15 @@ your test suite. with self.assertTemplateUsed(template_name='index.html'): render_to_string('index.html') -.. method:: TestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was *not* used in rendering the response. You can use this as a context manager in the same way as - :meth:`~TestCase.assertTemplateUsed`. + :meth:`~SimpleTestCase.assertTemplateUsed`. -.. method:: TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') +.. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') Asserts that the response return a ``status_code`` redirect status, it redirected to ``expected_url`` (including any GET data), and the final @@ -1557,44 +1608,6 @@ your test suite. ``target_status_code`` will be the url and status code for the final point of the redirect chain. -.. method:: TestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) - - Asserts that a queryset ``qs`` returns a particular list of values ``values``. - - The comparison of the contents of ``qs`` and ``values`` is performed using - the function ``transform``; by default, this means that the ``repr()`` of - each value is compared. Any other callable can be used if ``repr()`` doesn't - provide a unique or helpful comparison. - - By default, the comparison is also ordering dependent. If ``qs`` doesn't - provide an implicit ordering, you can set the ``ordered`` parameter to - ``False``, which turns the comparison into a Python set comparison. - - .. versionchanged:: 1.6 - - The method now checks for undefined order and raises ``ValueError`` - if undefined order is spotted. The ordering is seen as undefined if - the given ``qs`` isn't ordered and the comparison is against more - than one ordered values. - -.. method:: TestCase.assertNumQueries(num, func, *args, **kwargs) - - Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that - ``num`` database queries are executed. - - If a ``"using"`` key is present in ``kwargs`` it is used as the database - alias for which to check the number of queries. If you wish to call a - function with a ``using`` parameter you can do it by wrapping the call with - a ``lambda`` to add an extra parameter:: - - self.assertNumQueries(7, lambda: my_function(using=7)) - - You can also use this as a context manager:: - - with self.assertNumQueries(2): - Person.objects.create(name="Aaron") - Person.objects.create(name="Daniel") - .. method:: SimpleTestCase.assertHTMLEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are equal. The comparison @@ -1624,6 +1637,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are *not* equal. The @@ -1633,6 +1648,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1644,6 +1661,8 @@ your test suite. syntax differences. When unvalid XML is passed in any parameter, an ``AssertionError`` is always raised, even if both string are identical. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1652,6 +1671,68 @@ your test suite. comparison is based on XML semantics. See :meth:`~SimpleTestCase.assertXMLEqual` for details. + Output in case of error can be customized with the ``msg`` argument. + +.. method:: SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='') + + .. versionadded:: 1.5 + + Asserts that the HTML fragment ``needle`` is contained in the ``haystack`` one. + + If the ``count`` integer argument is specified, then additionally the number + of ``needle`` occurrences will be strictly verified. + + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + +.. method:: SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None) + + .. versionadded:: 1.5 + + Asserts that the JSON fragments ``raw`` and ``expected_data`` are equal. + Usual JSON non-significant whitespace rules apply as the heavyweight is + delegated to the :mod:`json` library. + + Output in case of error can be customized with the ``msg`` argument. + +.. method:: TransactionTestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) + + Asserts that a queryset ``qs`` returns a particular list of values ``values``. + + The comparison of the contents of ``qs`` and ``values`` is performed using + the function ``transform``; by default, this means that the ``repr()`` of + each value is compared. Any other callable can be used if ``repr()`` doesn't + provide a unique or helpful comparison. + + By default, the comparison is also ordering dependent. If ``qs`` doesn't + provide an implicit ordering, you can set the ``ordered`` parameter to + ``False``, which turns the comparison into a Python set comparison. + + .. versionchanged:: 1.6 + + The method now checks for undefined order and raises ``ValueError`` + if undefined order is spotted. The ordering is seen as undefined if + the given ``qs`` isn't ordered and the comparison is against more + than one ordered values. + +.. method:: TransactionTestCase.assertNumQueries(num, func, *args, **kwargs) + + Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that + ``num`` database queries are executed. + + If a ``"using"`` key is present in ``kwargs`` it is used as the database + alias for which to check the number of queries. If you wish to call a + function with a ``using`` parameter you can do it by wrapping the call with + a ``lambda`` to add an extra parameter:: + + self.assertNumQueries(7, lambda: my_function(using=7)) + + You can also use this as a context manager:: + + with self.assertNumQueries(2): + Person.objects.create(name="Aaron") + Person.objects.create(name="Daniel") + .. _topics-testing-email: Email services @@ -1701,7 +1782,7 @@ and contents:: self.assertEqual(mail.outbox[0].subject, 'Subject here') As noted :ref:`previously <emptying-test-outbox>`, the test outbox is emptied -at the start of every test in a Django ``TestCase``. To empty the outbox +at the start of every test in a Django ``*TestCase``. To empty the outbox manually, assign the empty list to ``mail.outbox``:: from django.core import mail diff --git a/setup.py b/setup.py index 7f848a56ff..b2b7821557 100644 --- a/setup.py +++ b/setup.py @@ -93,7 +93,7 @@ setup( package_data=package_data, scripts=['django/bin/django-admin.py'], classifiers=[ - 'Development Status :: 5 - Production/Stable', + 'Development Status :: 3 - Alpha', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index 8387ba77a1..175b1972c9 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -67,6 +67,11 @@ class ChordsBandAdmin(admin.ModelAdmin): list_filter = ['members'] +class InvitationAdmin(admin.ModelAdmin): + list_display = ('band', 'player') + list_select_related = ('player',) + + class DynamicListDisplayChildAdmin(admin.ModelAdmin): list_display = ('parent', 'name', 'age') diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index fd433967d3..7f3f0d162e 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -4,6 +4,7 @@ import datetime from django.contrib import admin from django.contrib.admin.options import IncorrectLookupParameters +from django.contrib.admin.templatetags.admin_list import pagination from django.contrib.admin.views.main import ChangeList, SEARCH_VAR, ALL_VAR from django.contrib.auth.models import User from django.core.urlresolvers import reverse @@ -17,7 +18,7 @@ from .admin import (ChildAdmin, QuartetAdmin, BandAdmin, ChordsBandAdmin, GroupAdmin, ParentAdmin, DynamicListDisplayChildAdmin, DynamicListDisplayLinksChildAdmin, CustomPaginationAdmin, FilteredChildAdmin, CustomPaginator, site as custom_site, - SwallowAdmin, DynamicListFilterChildAdmin) + SwallowAdmin, DynamicListFilterChildAdmin, InvitationAdmin) from .models import (Event, Child, Parent, Genre, Band, Musician, Group, Quartet, Membership, ChordsMusician, ChordsBand, Invitation, Swallow, UnorderedObject, OrderedObject, CustomIdUser) @@ -45,9 +46,32 @@ class ChangeListTests(TestCase): m = ChildAdmin(Child, admin.site) request = self.factory.get('/child/') cl = ChangeList(request, Child, m.list_display, m.list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, - m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m) - self.assertEqual(cl.queryset.query.select_related, {'parent': {'name': {}}}) + m.list_filter, m.date_hierarchy, m.search_fields, + m.list_select_related, m.list_per_page, + m.list_max_show_all, m.list_editable, m) + self.assertEqual(cl.queryset.query.select_related, { + 'parent': {'name': {}} + }) + + def test_select_related_as_tuple(self): + ia = InvitationAdmin(Invitation, admin.site) + request = self.factory.get('/invitation/') + cl = ChangeList(request, Child, ia.list_display, ia.list_display_links, + ia.list_filter, ia.date_hierarchy, ia.search_fields, + ia.list_select_related, ia.list_per_page, + ia.list_max_show_all, ia.list_editable, ia) + self.assertEqual(cl.queryset.query.select_related, {'player': {}}) + + def test_select_related_as_empty_tuple(self): + ia = InvitationAdmin(Invitation, admin.site) + ia.list_select_related = () + request = self.factory.get('/invitation/') + cl = ChangeList(request, Child, ia.list_display, ia.list_display_links, + ia.list_filter, ia.date_hierarchy, ia.search_fields, + ia.list_select_related, ia.list_per_page, + ia.list_max_show_all, ia.list_editable, ia) + self.assertEqual(cl.queryset.query.select_related, False) + def test_result_list_empty_changelist_value(self): """ @@ -564,6 +588,44 @@ class ChangeListTests(TestCase): response = m.changelist_view(request) self.assertEqual(response.context_data['cl'].list_filter, ('parent', 'name', 'age')) + def test_pagination_page_range(self): + """ + Regression tests for ticket #15653: ensure the number of pages + generated for changelist views are correct. + """ + # instantiating and setting up ChangeList object + m = GroupAdmin(Group, admin.site) + request = self.factory.get('/group/') + cl = ChangeList(request, Group, m.list_display, + m.list_display_links, m.list_filter, m.date_hierarchy, + m.search_fields, m.list_select_related, m.list_per_page, + m.list_max_show_all, m.list_editable, m) + per_page = cl.list_per_page = 10 + + for page_num, objects_count, expected_page_range in [ + (0, per_page, []), + (0, per_page * 2, list(range(2))), + (5, per_page * 11, list(range(11))), + (5, per_page * 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, '.', 10, 11]), + (6, per_page * 12, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, 10, 11]), + (6, per_page * 13, [0, 1, '.', 3, 4, 5, 6, 7, 8, 9, '.', 11, 12]), + ]: + # assuming we have exactly `objects_count` objects + Group.objects.all().delete() + for i in range(objects_count): + Group.objects.create(name='test band') + + # setting page number and calculating page range + cl.page_num = page_num + cl.get_results(request) + real_page_range = pagination(cl)['page_range'] + + self.assertListEqual( + expected_page_range, + list(real_page_range), + ) + + class AdminLogNodeTestCase(TestCase): def test_get_admin_log_templatetag_custom_user(self): diff --git a/tests/special_headers/__init__.py b/tests/admin_docs/__init__.py similarity index 100% rename from tests/special_headers/__init__.py rename to tests/admin_docs/__init__.py diff --git a/tests/special_headers/fixtures/data.xml b/tests/admin_docs/fixtures/data.xml similarity index 89% rename from tests/special_headers/fixtures/data.xml rename to tests/admin_docs/fixtures/data.xml index 7e60d45199..aba8f4aace 100644 --- a/tests/special_headers/fixtures/data.xml +++ b/tests/admin_docs/fixtures/data.xml @@ -14,7 +14,4 @@ <field to="auth.group" name="groups" rel="ManyToManyRel"></field> <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> </object> - <object pk="1" model="special_headers.article"> - <field type="TextField" name="text">text</field> - </object> </django-objects> diff --git a/tests/admin_docs/models.py b/tests/admin_docs/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py new file mode 100644 index 0000000000..aeb527c7b9 --- /dev/null +++ b/tests/admin_docs/tests.py @@ -0,0 +1,45 @@ +from django.contrib.auth.models import User +from django.test import TestCase +from django.test.utils import override_settings + + +@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) +class XViewMiddlewareTest(TestCase): + fixtures = ['data.xml'] + urls = 'admin_docs.urls' + + def test_xview_func(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/func/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.xview') + user.is_staff = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/func/') + self.assertFalse('X-View' in response) + + def test_xview_class(self): + user = User.objects.get(username='super') + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + self.client.login(username='super', password='secret') + response = self.client.head('/xview/class/') + self.assertTrue('X-View' in response) + self.assertEqual(response['X-View'], 'admin_docs.views.XViewClass') + user.is_staff = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) + user.is_staff = True + user.is_active = False + user.save() + response = self.client.head('/xview/class/') + self.assertFalse('X-View' in response) diff --git a/tests/admin_docs/urls.py b/tests/admin_docs/urls.py new file mode 100644 index 0000000000..3c3a8fe5d8 --- /dev/null +++ b/tests/admin_docs/urls.py @@ -0,0 +1,11 @@ +# coding: utf-8 +from __future__ import absolute_import + +from django.conf.urls import patterns + +from . import views + +urlpatterns = patterns('', + (r'^xview/func/$', views.xview_dec(views.xview)), + (r'^xview/class/$', views.xview_dec(views.XViewClass.as_view())), +) diff --git a/tests/special_headers/views.py b/tests/admin_docs/views.py similarity index 54% rename from tests/special_headers/views.py rename to tests/admin_docs/views.py index a8bbd6542e..e47177c37f 100644 --- a/tests/special_headers/views.py +++ b/tests/admin_docs/views.py @@ -1,21 +1,13 @@ -from django.core.xheaders import populate_xheaders from django.http import HttpResponse from django.utils.decorators import decorator_from_middleware from django.views.generic import View -from django.middleware.doc import XViewMiddleware - -from .models import Article +from django.contrib.admindocs.middleware import XViewMiddleware xview_dec = decorator_from_middleware(XViewMiddleware) def xview(request): return HttpResponse() -def xview_xheaders(request, object_id): - response = HttpResponse() - populate_xheaders(request, response, Article, 1) - return response - class XViewClass(View): def get(self, request): return HttpResponse() diff --git a/tests/admin_inlines/admin.py b/tests/admin_inlines/admin.py index 44671d0ac4..2f88248ca4 100644 --- a/tests/admin_inlines/admin.py +++ b/tests/admin_inlines/admin.py @@ -129,6 +129,22 @@ class ChildModel1Inline(admin.TabularInline): class ChildModel2Inline(admin.StackedInline): model = ChildModel2 +# admin for #19425 and #18388 +class BinaryTreeAdmin(admin.TabularInline): + model = BinaryTree + + def get_extra(self, request, obj=None, **kwargs): + extra = 2 + if obj: + return extra - obj.binarytree_set.count() + return extra + + def get_max_num(self, request, obj=None, **kwargs): + max_num = 3 + if obj: + return max_num - obj.binarytree_set.count() + return max_num + # admin for #19524 class SightingInline(admin.TabularInline): model = Sighting @@ -150,4 +166,5 @@ site.register(Author, AuthorAdmin) site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline, ReadOnlyInlineInline]) site.register(ProfileCollection, inlines=[ProfileInline]) site.register(ParentModelWithCustomPk, inlines=[ChildModel1Inline, ChildModel2Inline]) +site.register(BinaryTree, inlines=[BinaryTreeAdmin]) site.register(ExtraTerrestrial, inlines=[SightingInline]) diff --git a/tests/admin_inlines/models.py b/tests/admin_inlines/models.py index 82c1c3f078..d4ba0ab6bc 100644 --- a/tests/admin_inlines/models.py +++ b/tests/admin_inlines/models.py @@ -183,6 +183,12 @@ class ChildModel2(models.Model): def get_absolute_url(self): return '/child_model2/' + +# Models for #19425 +class BinaryTree(models.Model): + name = models.CharField(max_length=100) + parent = models.ForeignKey('self', null=True, blank=True) + # Models for #19524 class LifeForm(models.Model): diff --git a/tests/admin_inlines/tests.py b/tests/admin_inlines/tests.py index 714a2f1c61..78ccf074d5 100644 --- a/tests/admin_inlines/tests.py +++ b/tests/admin_inlines/tests.py @@ -12,7 +12,7 @@ from .admin import InnerInline, TitleInline, site from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile, ProfileCollection, ParentModelWithCustomPk, ChildModel1, ChildModel2, - Sighting, Title, Novel, Chapter, FootNote) + Sighting, Title, Novel, Chapter, FootNote, BinaryTree) @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -193,6 +193,24 @@ class TestInline(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(Sighting.objects.filter(et__name='Martian').count(), 1) + def test_custom_get_extra_form(self): + bt_head = BinaryTree.objects.create(name="Tree Head") + bt_child = BinaryTree.objects.create(name="First Child", parent=bt_head) + + # The maximum number of forms should respect 'get_max_num' on the + # ModelAdmin + max_forms_input = '<input id="id_binarytree_set-MAX_NUM_FORMS" name="binarytree_set-MAX_NUM_FORMS" type="hidden" value="%d" />' + # The total number of forms will remain the same in either case + total_forms_hidden = '<input id="id_binarytree_set-TOTAL_FORMS" name="binarytree_set-TOTAL_FORMS" type="hidden" value="2" />' + + response = self.client.get('/admin/admin_inlines/binarytree/add/') + self.assertContains(response, max_forms_input % 3) + self.assertContains(response, total_forms_hidden) + + response = self.client.get("/admin/admin_inlines/binarytree/%d/" % bt_head.id) + self.assertContains(response, max_forms_input % 2) + self.assertContains(response, total_forms_hidden) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class TestInlineMedia(TestCase): diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index c8986770f3..9e6b1f557b 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -1305,13 +1305,15 @@ class CommandTypes(AdminScriptTestCase): sys.stderr = err = StringIO() try: command.execute = lambda args: args # This will trigger TypeError - with self.assertRaises(SystemExit): - command.run_from_argv(['', '']) - err_message = err.getvalue() - # Exceptions other than CommandError automatically output the traceback - self.assertIn("Traceback", err_message) - self.assertIn("TypeError", err_message) + # If the Exception is not CommandError it should always + # raise the original exception. + with self.assertRaises(TypeError): + command.run_from_argv(['', '']) + + # If the Exception is CommandError and --traceback is not present + # this command should raise a SystemExit and don't print any + # traceback to the stderr. command.execute = raise_command_error err.truncate(0) with self.assertRaises(SystemExit): @@ -1320,12 +1322,12 @@ class CommandTypes(AdminScriptTestCase): self.assertNotIn("Traceback", err_message) self.assertIn("CommandError", err_message) + # If the Exception is CommandError and --traceback is present + # this command should raise the original CommandError as if it + # were not a CommandError. err.truncate(0) - with self.assertRaises(SystemExit): + with self.assertRaises(CommandError): command.run_from_argv(['', '', '--traceback']) - err_message = err.getvalue() - self.assertIn("Traceback (most recent call last)", err_message) - self.assertIn("CommandError", err_message) finally: sys.stderr = old_stderr @@ -1680,3 +1682,22 @@ class DiffSettings(AdminScriptTestCase): out, err = self.run_manage(args) self.assertNoOutput(err) self.assertOutput(out, "### STATIC_URL = None") + +class Dumpdata(AdminScriptTestCase): + """Tests for dumpdata management command.""" + + def setUp(self): + self.write_settings('settings.py') + + def tearDown(self): + self.remove_settings('settings.py') + + def test_pks_parsing(self): + """Regression for #20509 + + Test would raise an exception rather than printing an error message. + """ + args = ['dumpdata', '--pks=1'] + out, err = self.run_manage(args) + self.assertOutput(err, "You can only use --pks option with one model") + self.assertNoOutput(out) diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py index 7898f200b5..4a9a203f50 100644 --- a/tests/admin_util/tests.py +++ b/tests/admin_util/tests.py @@ -11,8 +11,7 @@ from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE from django.contrib.sites.models import Site from django.db import models, DEFAULT_DB_ALIAS from django import forms -from django.test import TestCase -from django.utils import unittest +from django.test import SimpleTestCase, TestCase from django.utils.formats import localize from django.utils.safestring import mark_safe from django.utils import six @@ -82,7 +81,7 @@ class NestedObjectsTests(TestCase): # One for Location, one for Guest, and no query for EventGuide n.collect(objs) -class UtilTests(unittest.TestCase): +class UtilTests(SimpleTestCase): def test_values_from_lookup_field(self): """ Regression test for #12654: lookup_field @@ -151,7 +150,7 @@ class UtilTests(unittest.TestCase): # handling. display_value = display_for_field(None, models.NullBooleanField()) expected = '<img src="%sadmin/img/icon-unknown.gif" alt="None" />' % settings.STATIC_URL - self.assertEqual(display_value, expected) + self.assertHTMLEqual(display_value, expected) display_value = display_for_field(None, models.DecimalField()) self.assertEqual(display_value, EMPTY_CHANGELIST_VALUE) @@ -236,6 +235,20 @@ class UtilTests(unittest.TestCase): ("not Really the Model", MockModelAdmin.test_from_model) ) + def test_label_for_property(self): + # NOTE: cannot use @property decorator, because of + # AttributeError: 'property' object has no attribute 'short_description' + class MockModelAdmin(object): + def my_property(self): + return "this if from property" + my_property.short_description = 'property short description' + test_from_property = property(my_property) + + self.assertEqual( + label_for_field("test_from_property", Article, model_admin=MockModelAdmin), + 'property short description' + ) + def test_related_name(self): """ Regression test for #13963 @@ -285,10 +298,10 @@ class UtilTests(unittest.TestCase): cb = forms.BooleanField(label=mark_safe('<i>cb</i>')) form = MyForm() - self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), - '<label for="id_text" class="required inline"><i>text</i>:</label>') - self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), - '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i></label>') + self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), + '<label for="id_text" class="required inline"><i>text</i>:</label>') + self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), + '<label for="id_cb" class="vCheckboxLabel required inline"><i>cb</i></label>') # normal strings needs to be escaped class MyForm(forms.Form): @@ -296,10 +309,10 @@ class UtilTests(unittest.TestCase): cb = forms.BooleanField(label='&cb') form = MyForm() - self.assertEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), - '<label for="id_text" class="required inline">&text:</label>') - self.assertEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), - '<label for="id_cb" class="vCheckboxLabel required inline">&cb</label>') + self.assertHTMLEqual(helpers.AdminField(form, 'text', is_first=False).label_tag(), + '<label for="id_text" class="required inline">&text:</label>') + self.assertHTMLEqual(helpers.AdminField(form, 'cb', is_first=False).label_tag(), + '<label for="id_cb" class="vCheckboxLabel required inline">&cb</label>') def test_flatten_fieldsets(self): """ diff --git a/tests/admin_validation/tests.py b/tests/admin_validation/tests.py index 16f73c6390..5eee3e7105 100644 --- a/tests/admin_validation/tests.py +++ b/tests/admin_validation/tests.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from django import forms from django.contrib import admin -from django.contrib.admin.validation import validate, validate_inline from django.core.exceptions import ImproperlyConfigured from django.test import TestCase @@ -38,13 +37,13 @@ class ValidationTestCase(TestCase): "fields": ["title", "original_release"], }), ] - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_custom_modelforms_with_fields_fieldsets(self): """ # Regression test for #8027: custom ModelForms with fields/fieldsets """ - validate(ValidFields, Song) + ValidFields.validate(Song) def test_custom_get_form_with_fieldsets(self): """ @@ -52,7 +51,7 @@ class ValidationTestCase(TestCase): is overridden. Refs #19445. """ - validate(ValidFormFieldsets, Song) + ValidFormFieldsets.validate(Song) def test_exclude_values(self): """ @@ -62,16 +61,16 @@ class ValidationTestCase(TestCase): exclude = ('foo') self.assertRaisesMessage(ImproperlyConfigured, "'ExcludedFields1.exclude' must be a list or tuple.", - validate, - ExcludedFields1, Book) + ExcludedFields1.validate, + Book) def test_exclude_duplicate_values(self): class ExcludedFields2(admin.ModelAdmin): exclude = ('name', 'name') self.assertRaisesMessage(ImproperlyConfigured, "There are duplicate field(s) in ExcludedFields2.exclude", - validate, - ExcludedFields2, Book) + ExcludedFields2.validate, + Book) def test_exclude_in_inline(self): class ExcludedFieldsInline(admin.TabularInline): @@ -84,8 +83,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'ExcludedFieldsInline.exclude' must be a list or tuple.", - validate, - ExcludedFieldsAlbumAdmin, Album) + ExcludedFieldsAlbumAdmin.validate, + Album) def test_exclude_inline_model_admin(self): """ @@ -102,8 +101,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "SongInline cannot exclude the field 'album' - this is the foreign key to the parent model admin_validation.Album.", - validate, - AlbumAdmin, Album) + AlbumAdmin.validate, + Album) def test_app_label_in_admin_validation(self): """ @@ -114,8 +113,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'RawIdNonexistingAdmin.raw_id_fields' refers to field 'nonexisting' that is missing from model 'admin_validation.Album'.", - validate, - RawIdNonexistingAdmin, Album) + RawIdNonexistingAdmin.validate, + Album) def test_fk_exclusion(self): """ @@ -127,28 +126,35 @@ class ValidationTestCase(TestCase): model = TwoAlbumFKAndAnE exclude = ("e",) fk_name = "album1" - validate_inline(TwoAlbumFKAndAnEInline, None, Album) + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + MyAdmin.validate(Album) + def test_inline_self_validation(self): class TwoAlbumFKAndAnEInline(admin.TabularInline): model = TwoAlbumFKAndAnE + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] self.assertRaisesMessage(Exception, "<class 'admin_validation.models.TwoAlbumFKAndAnE'> has more than 1 ForeignKey to <class 'admin_validation.models.Album'>", - validate_inline, - TwoAlbumFKAndAnEInline, None, Album) + MyAdmin.validate, Album) def test_inline_with_specified(self): class TwoAlbumFKAndAnEInline(admin.TabularInline): model = TwoAlbumFKAndAnE fk_name = "album1" - validate_inline(TwoAlbumFKAndAnEInline, None, Album) + + class MyAdmin(admin.ModelAdmin): + inlines = [TwoAlbumFKAndAnEInline] + MyAdmin.validate(Album) def test_readonly(self): class SongAdmin(admin.ModelAdmin): readonly_fields = ("title",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_on_method(self): def my_function(obj): @@ -157,7 +163,7 @@ class ValidationTestCase(TestCase): class SongAdmin(admin.ModelAdmin): readonly_fields = (my_function,) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_on_modeladmin(self): class SongAdmin(admin.ModelAdmin): @@ -166,13 +172,13 @@ class ValidationTestCase(TestCase): def readonly_method_on_modeladmin(self, obj): pass - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_method_on_model(self): class SongAdmin(admin.ModelAdmin): readonly_fields = ("readonly_method_on_model",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_nonexistant_field(self): class SongAdmin(admin.ModelAdmin): @@ -180,8 +186,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.", - validate, - SongAdmin, Song) + SongAdmin.validate, + Song) def test_nonexistant_field_on_inline(self): class CityInline(admin.TabularInline): @@ -190,8 +196,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "CityInline.readonly_fields[0], 'i_dont_exist' is not a callable or an attribute of 'CityInline' or found in the model 'City'.", - validate_inline, - CityInline, None, State) + CityInline.validate, + City) def test_extra(self): class SongAdmin(admin.ModelAdmin): @@ -199,13 +205,13 @@ class ValidationTestCase(TestCase): if instance.title == "Born to Run": return "Best Ever!" return "Status unknown." - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_readonly_lambda(self): class SongAdmin(admin.ModelAdmin): readonly_fields = (lambda obj: "test",) - validate(SongAdmin, Song) + SongAdmin.validate(Song) def test_graceful_m2m_fail(self): """ @@ -219,8 +225,8 @@ class ValidationTestCase(TestCase): self.assertRaisesMessage(ImproperlyConfigured, "'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", - validate, - BookAdmin, Book) + BookAdmin.validate, + Book) def test_cannot_include_through(self): class FieldsetBookAdmin(admin.ModelAdmin): @@ -230,20 +236,20 @@ class ValidationTestCase(TestCase): ) self.assertRaisesMessage(ImproperlyConfigured, "'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", - validate, - FieldsetBookAdmin, Book) + FieldsetBookAdmin.validate, + Book) def test_nested_fields(self): class NestedFieldsAdmin(admin.ModelAdmin): fields = ('price', ('name', 'subtitle')) - validate(NestedFieldsAdmin, Book) + NestedFieldsAdmin.validate(Book) def test_nested_fieldsets(self): class NestedFieldsetAdmin(admin.ModelAdmin): fieldsets = ( ('Main', {'fields': ('price', ('name', 'subtitle'))}), ) - validate(NestedFieldsetAdmin, Book) + NestedFieldsetAdmin.validate(Book) def test_explicit_through_override(self): """ @@ -260,7 +266,7 @@ class ValidationTestCase(TestCase): # If the through model is still a string (and hasn't been resolved to a model) # the validation will fail. - validate(BookAdmin, Book) + BookAdmin.validate(Book) def test_non_model_fields(self): """ @@ -274,7 +280,7 @@ class ValidationTestCase(TestCase): form = SongForm fields = ['title', 'extra_data'] - validate(FieldsOnFormOnlyAdmin, Song) + FieldsOnFormOnlyAdmin.validate(Song) def test_non_model_first_field(self): """ @@ -292,4 +298,4 @@ class ValidationTestCase(TestCase): form = SongForm fields = ['extra_data', 'title'] - validate(FieldsOnFormOnlyAdmin, Song) + FieldsOnFormOnlyAdmin.validate(Song) diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index cc7585cd2d..a6ad7cc0bc 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -9,11 +9,13 @@ from django.contrib import admin from django.contrib.admin.views.main import ChangeList from django.core.files.storage import FileSystemStorage from django.core.mail import EmailMessage +from django.core.servers.basehttp import FileWrapper from django.conf.urls import patterns, url from django.db import models from django.forms.models import BaseModelFormSet -from django.http import HttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.contrib.admin import BooleanFieldListFilter +from django.utils.six import StringIO from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Widget, DooHickey, Grommet, Whatsit, FancyDoodad, Category, Link, @@ -24,7 +26,7 @@ from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, Gadget, Villain, SuperVillain, Plot, PlotDetails, CyclicOne, CyclicTwo, WorkHour, Reservation, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory, Book, Promo, ChapterXtra1, Pizza, Topping, - Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, + Album, Question, Answer, ComplexSortedPerson, PluggableSearchPerson, PrePopulatedPostLargeSlug, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, RelatedPrepopulated, UndeletableObject, UserMessenger, Simple, Choice, @@ -149,7 +151,7 @@ class InquisitionAdmin(admin.ModelAdmin): class SketchAdmin(admin.ModelAdmin): - raw_id_fields = ('inquisition',) + raw_id_fields = ('inquisition', 'defendant0', 'defendant1') class FabricAdmin(admin.ModelAdmin): @@ -238,8 +240,20 @@ def redirect_to(modeladmin, request, selected): redirect_to.short_description = 'Redirect to (Awesome action)' +def download(modeladmin, request, selected): + buf = StringIO('This is the content of the file') + return StreamingHttpResponse(FileWrapper(buf)) +download.short_description = 'Download subscription' + + +def no_perm(modeladmin, request, selected): + return HttpResponse(content='No permission to perform this action', + status=403) +no_perm.short_description = 'No permission to run' + + class ExternalSubscriberAdmin(admin.ModelAdmin): - actions = [redirect_to, external_mail] + actions = [redirect_to, external_mail, download, no_perm] class Podcast(Media): @@ -530,6 +544,20 @@ class ComplexSortedPersonAdmin(admin.ModelAdmin): colored_name.admin_order_field = 'name' +class PluggableSearchPersonAdmin(admin.ModelAdmin): + list_display = ('name', 'age') + search_fields = ('name',) + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super(PluggableSearchPersonAdmin, self).get_search_results(request, queryset, search_term) + try: + search_term_as_int = int(search_term) + queryset |= self.model.objects.filter(age=search_term_as_int) + except: + pass + return queryset, use_distinct + + class AlbumAdmin(admin.ModelAdmin): list_filter = ['title'] @@ -733,6 +761,7 @@ site.register(Question) site.register(Answer) site.register(PrePopulatedPost, PrePopulatedPostAdmin) site.register(ComplexSortedPerson, ComplexSortedPersonAdmin) +site.register(PluggableSearchPerson, PluggableSearchPersonAdmin) site.register(PrePopulatedPostLargeSlug, PrePopulatedPostLargeSlugAdmin) site.register(AdminOrderedField, AdminOrderedFieldAdmin) site.register(AdminOrderedModelMethod, AdminOrderedModelMethodAdmin) diff --git a/tests/admin_views/models.py b/tests/admin_views/models.py index 1916949f63..5cc6f6251a 100644 --- a/tests/admin_views/models.py +++ b/tests/admin_views/models.py @@ -137,6 +137,7 @@ class Thing(models.Model): class Actor(models.Model): name = models.CharField(max_length=50) age = models.IntegerField() + title = models.CharField(max_length=50, null=True) def __str__(self): return self.name @@ -158,6 +159,8 @@ class Sketch(models.Model): 'leader__age': 27, 'expected': False, }) + defendant0 = models.ForeignKey(Actor, limit_choices_to={'title__isnull': False}, related_name='as_defendant0') + defendant1 = models.ForeignKey(Actor, limit_choices_to={'title__isnull': True}, related_name='as_defendant1') def __str__(self): return self.title @@ -591,6 +594,12 @@ class ComplexSortedPerson(models.Model): age = models.PositiveIntegerField() is_employee = models.NullBooleanField() + +class PluggableSearchPerson(models.Model): + name = models.CharField(max_length=100) + age = models.PositiveIntegerField() + + class PrePopulatedPostLargeSlug(models.Model): """ Regression test for #15938: a large max_length for the slugfield must not diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index 8e678a72b3..8c8a65318c 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -11,7 +11,6 @@ except ImportError: # Python 2 from django.conf import settings, global_settings from django.core import mail -from django.core.exceptions import SuspiciousOperation from django.core.files import temp as tempfile from django.core.urlresolvers import reverse # Register auth models with the admin. @@ -30,6 +29,7 @@ from django.db import connection from django.forms.util import ErrorList from django.template.response import TemplateResponse from django.test import TestCase +from django.test.utils import patch_logger from django.utils import formats, translation, unittest from django.utils.cache import get_max_age from django.utils.encoding import iri_to_uri, force_bytes @@ -46,7 +46,7 @@ from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, DooHickey, FancyDoodad, Whatsit, Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee, Question, Answer, Inquisition, Actor, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, Story, - OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, + OtherStory, ComplexSortedPerson, PluggableSearchPerson, Parent, Child, AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, Simple, UndeletableObject, Choice, ShortMessage, Telegram) @@ -468,8 +468,12 @@ class AdminViewBasicTest(TestCase): self.assertContains(response, '4 articles') response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'false'}) self.assertContains(response, '3 articles') + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': '0'}) + self.assertContains(response, '3 articles') response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'true'}) self.assertContains(response, '1 article') + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': '1'}) + self.assertContains(response, '1 article') def testLogoutAndPasswordChangeURLs(self): response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit) @@ -543,20 +547,21 @@ class AdminViewBasicTest(TestCase): self.assertContains(response, '%Y-%m-%d %H:%M:%S') def test_disallowed_filtering(self): - self.assertRaises(SuspiciousOperation, - self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy" - ) + with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as calls: + response = self.client.get("/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy") + self.assertEqual(response.status_code, 400) + self.assertEqual(len(calls), 1) - try: - self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red") - self.client.get("/test_admin/admin/admin_views/thing/?color__value=red") - except SuspiciousOperation: - self.fail("Filters are allowed if explicitly included in list_filter") + # Filters are allowed if explicitly included in list_filter + response = self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red") + self.assertEqual(response.status_code, 200) + response = self.client.get("/test_admin/admin/admin_views/thing/?color__value=red") + self.assertEqual(response.status_code, 200) - try: - self.client.get("/test_admin/admin/admin_views/person/?age__gt=30") - except SuspiciousOperation: - self.fail("Filters should be allowed if they involve a local field without the need to whitelist them in list_filter or date_hierarchy.") + # Filters should be allowed if they involve a local field without the + # need to whitelist them in list_filter or date_hierarchy. + response = self.client.get("/test_admin/admin/admin_views/person/?age__gt=30") + self.assertEqual(response.status_code, 200) e1 = Employee.objects.create(name='Anonymous', gender=1, age=22, alive=True, code='123') e2 = Employee.objects.create(name='Visitor', gender=2, age=19, alive=True, code='124') @@ -574,10 +579,9 @@ class AdminViewBasicTest(TestCase): ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields can break. """ - try: - self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") - except SuspiciousOperation: - self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model") + # Filters should be allowed if they are defined on a ForeignKey pointing to this model + response = self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27") + self.assertEqual(response.status_code, 200) def test_hide_change_password(self): """ @@ -1995,7 +1999,7 @@ class AdminViewListEditable(TestCase): } response = self.client.post('/test_admin/admin/admin_views/person/', data) non_form_errors = response.context['cl'].formset.non_form_errors() - self.assertTrue(isinstance(non_form_errors, ErrorList)) + self.assertIsInstance(non_form_errors, ErrorList) self.assertEqual(str(non_form_errors), str(ErrorList(["Grace is not a Zombie"]))) def test_list_editable_ordering(self): @@ -2202,6 +2206,20 @@ class AdminSearchTest(TestCase): self.assertContains(response, "\n0 persons\n") self.assertNotContains(response, "Guido") + def test_pluggable_search(self): + p1 = PluggableSearchPerson.objects.create(name="Bob", age=10) + p2 = PluggableSearchPerson.objects.create(name="Amy", age=20) + + response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=Bob') + # confirm the search returned one object + self.assertContains(response, "\n1 pluggable search person\n") + self.assertContains(response, "Bob") + + response = self.client.get('/test_admin/admin/admin_views/pluggablesearchperson/?q=20') + # confirm the search returned one object + self.assertContains(response, "\n1 pluggable search person\n") + self.assertContains(response, "Amy") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminInheritedInlinesTest(TestCase): @@ -2414,6 +2432,29 @@ class AdminActionsTest(TestCase): response = self.client.post(url, action_data) self.assertRedirects(response, url) + def test_custom_function_action_streaming_response(self): + """Tests a custom action that returns a StreamingHttpResponse.""" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'download', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + content = b''.join(response.streaming_content) + self.assertEqual(content, b'This is the content of the file') + self.assertEqual(response.status_code, 200) + + def test_custom_function_action_no_perm_response(self): + """Tests a custom action that returns an HttpResponse with 403 code.""" + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action': 'no_perm', + 'index': 0, + } + response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) + self.assertEqual(response.status_code, 403) + self.assertEqual(response.content, b'No permission to perform this action') + def test_actions_ordering(self): """ Ensure that actions are ordered as expected. @@ -2422,9 +2463,13 @@ class AdminActionsTest(TestCase): response = self.client.get('/test_admin/admin/admin_views/externalsubscriber/') self.assertContains(response, '''<label>Action: <select name="action"> <option value="" selected="selected">---------</option> -<option value="delete_selected">Delete selected external subscribers</option> +<option value="delete_selected">Delete selected external +subscribers</option> <option value="redirect_to">Redirect to (Awesome action)</option> -<option value="external_mail">External mail (Another awesome action)</option> +<option value="external_mail">External mail (Another awesome +action)</option> +<option value="download">Download subscription</option> +<option value="no_perm">No permission to run</option> </select>''', html=True) def test_model_without_action(self): @@ -3494,7 +3539,6 @@ class RawIdFieldsTest(TestCase): def test_limit_choices_to(self): """Regression test for 14880""" - # This includes tests integers, strings and booleans in the lookup query string actor = Actor.objects.create(name="Palin", age=27) inquisition1 = Inquisition.objects.create(expected=True, leader=actor, @@ -3510,11 +3554,57 @@ class RawIdFieldsTest(TestCase): # Handle relative links popup_url = urljoin(response.request['PATH_INFO'], popup_url) - # Get the popup + # Get the popup and verify the correct objects show up in the resulting + # page. This step also tests integers, strings and booleans in the + # lookup query string; in model we define inquisition field to have a + # limit_choices_to option that includes a filter on a string field + # (inquisition__actor__name), a filter on an integer field + # (inquisition__actor__age), and a filter on a boolean field + # (inquisition__expected). response2 = self.client.get(popup_url) self.assertContains(response2, "Spain") self.assertNotContains(response2, "England") + def test_limit_choices_to_isnull_false(self): + """Regression test for 20182""" + Actor.objects.create(name="Palin", age=27) + Actor.objects.create(name="Kilbraken", age=50, title="Judge") + response = self.client.get('/test_admin/admin/admin_views/sketch/add/') + # Find the link + m = re.search(br'<a href="([^"]*)"[^>]* id="lookup_id_defendant0"', response.content) + self.assertTrue(m) # Got a match + popup_url = m.groups()[0].decode().replace("&", "&") + + # Handle relative links + popup_url = urljoin(response.request['PATH_INFO'], popup_url) + # Get the popup and verify the correct objects show up in the resulting + # page. This step tests field__isnull=0 gets parsed correctly from the + # lookup query string; in model we define defendant0 field to have a + # limit_choices_to option that includes "actor__title__isnull=False". + response2 = self.client.get(popup_url) + self.assertContains(response2, "Kilbraken") + self.assertNotContains(response2, "Palin") + + def test_limit_choices_to_isnull_true(self): + """Regression test for 20182""" + Actor.objects.create(name="Palin", age=27) + Actor.objects.create(name="Kilbraken", age=50, title="Judge") + response = self.client.get('/test_admin/admin/admin_views/sketch/add/') + # Find the link + m = re.search(br'<a href="([^"]*)"[^>]* id="lookup_id_defendant1"', response.content) + self.assertTrue(m) # Got a match + popup_url = m.groups()[0].decode().replace("&", "&") + + # Handle relative links + popup_url = urljoin(response.request['PATH_INFO'], popup_url) + # Get the popup and verify the correct objects show up in the resulting + # page. This step tests field__isnull=1 gets parsed correctly from the + # lookup query string; in model we define defendant1 field to have a + # limit_choices_to option that includes "actor__title__isnull=True". + response2 = self.client.get(popup_url) + self.assertNotContains(response2, "Kilbraken") + self.assertContains(response2, "Palin") + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class UserAdminTest(TestCase): diff --git a/tests/admin_widgets/tests.py b/tests/admin_widgets/tests.py index 4e09922893..98f41c1490 100644 --- a/tests/admin_widgets/tests.py +++ b/tests/admin_widgets/tests.py @@ -13,6 +13,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import CharField, DateField from django.test import TestCase as DjangoTestCase from django.test.utils import override_settings +from django.utils import six from django.utils import translation from django.utils.html import conditional_escape from django.utils.unittest import TestCase @@ -139,6 +140,17 @@ class AdminFormfieldForDBFieldTests(TestCase): def testInheritance(self): self.assertFormfield(models.Album, 'backside_art', widgets.AdminFileWidget) + def test_m2m_widgets(self): + """m2m fields help text as it applies to admin app (#9321).""" + class AdvisorAdmin(admin.ModelAdmin): + filter_vertical=['companies'] + + self.assertFormfield(models.Advisor, 'companies', widgets.FilteredSelectMultiple, + filter_vertical=['companies']) + ma = AdvisorAdmin(models.Advisor, admin.site) + f = ma.formfield_for_dbfield(models.Advisor._meta.get_field('companies'), request=None) + self.assertEqual(six.text_type(f.help_text), ' Hold down "Control", or "Command" on a Mac, to select more than one.') + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class AdminFormfieldForDBFieldWithRequestTests(DjangoTestCase): diff --git a/tests/aggregation_regress/tests.py b/tests/aggregation_regress/tests.py index 6c5a43664a..80441b9a60 100644 --- a/tests/aggregation_regress/tests.py +++ b/tests/aggregation_regress/tests.py @@ -581,6 +581,7 @@ class AggregationTests(TestCase): 6 ) + # Note: intentionally no order_by(), that case needs tests, too. publishers = Publisher.objects.filter(id__in=[1, 2]) self.assertEqual( sorted(p.name for p in publishers), @@ -591,10 +592,15 @@ class AggregationTests(TestCase): ) publishers = publishers.annotate(n_books=Count("book")) + sorted_publishers = sorted(publishers, key=lambda x: x.name) self.assertEqual( - publishers[0].n_books, + sorted_publishers[0].n_books, 2 ) + self.assertEqual( + sorted_publishers[1].n_books, + 1 + ) self.assertEqual( sorted(p.name for p in publishers), diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 81b2851403..08cae0f7c3 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -8,7 +8,7 @@ import threading from django.conf import settings from django.core.management.color import no_style -from django.db import (backend, connection, connections, DEFAULT_DB_ALIAS, +from django.db import (connection, connections, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, transaction) from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version @@ -50,7 +50,8 @@ class OracleChecks(unittest.TestCase): def test_dbms_session(self): # If the backend is Oracle, test that we can call a standard # stored procedure through our cursor wrapper. - convert_unicode = backend.convert_unicode + from django.db.backends.oracle.base import convert_unicode + cursor = connection.cursor() cursor.callproc(convert_unicode('DBMS_SESSION.SET_IDENTIFIER'), [convert_unicode('_django_testing!')]) @@ -60,8 +61,10 @@ class OracleChecks(unittest.TestCase): def test_cursor_var(self): # If the backend is Oracle, test that we can pass cursor variables # as query parameters. + from django.db.backends.oracle.base import Database + cursor = connection.cursor() - var = cursor.var(backend.Database.STRING) + var = cursor.var(Database.STRING) cursor.execute("BEGIN %s := 'X'; END; ", [var]) self.assertEqual(var.getvalue(), 'X') @@ -172,7 +175,7 @@ class LastExecutedQueryTest(TestCase): sql, params = persons.query.sql_with_params() cursor = persons.query.get_compiler('default').execute_sql(None) last_sql = cursor.db.ops.last_executed_query(cursor, sql, params) - self.assertTrue(isinstance(last_sql, six.text_type)) + self.assertIsInstance(last_sql, six.text_type) @unittest.skipUnless(connection.vendor == 'sqlite', "This test is specific to SQLite.") diff --git a/tests/base/models.py b/tests/base/models.py index bddb406820..d47ddcfd66 100644 --- a/tests/base/models.py +++ b/tests/base/models.py @@ -14,8 +14,10 @@ class CustomBaseModel(models.base.ModelBase): class MyModel(six.with_metaclass(CustomBaseModel, models.Model)): - """Model subclass with a custom base using six.with_metaclass.""" + """Model subclass with a custom base using six.with_metaclass.""" +# This is done to ensure that for Python2 only, defining metaclasses +# still does not fail to create the model. if not six.PY3: class MyModel(models.Model): diff --git a/tests/basic/models.py b/tests/basic/models.py index 660beddf49..1bffcb9cda 100644 --- a/tests/basic/models.py +++ b/tests/basic/models.py @@ -18,3 +18,11 @@ class Article(models.Model): def __str__(self): return self.headline + +@python_2_unicode_compatible +class SelfRef(models.Model): + selfref = models.ForeignKey('self', null=True, blank=True, + related_name='+') + + def __str__(self): + return SelfRef.objects.get(selfref=self).pk diff --git a/tests/basic/tests.py b/tests/basic/tests.py index ccbb9bd423..6bf46cce9b 100644 --- a/tests/basic/tests.py +++ b/tests/basic/tests.py @@ -11,7 +11,7 @@ from django.test import TestCase, TransactionTestCase, skipIfDBFeature, skipUnle from django.utils import six from django.utils.translation import ugettext_lazy -from .models import Article +from .models import Article, SelfRef class ModelTest(TestCase): @@ -87,23 +87,14 @@ class ModelTest(TestCase): # parameters don't match any object. six.assertRaisesRegex(self, ObjectDoesNotExist, - "Article matching query does not exist. Lookup parameters were " - "{'id__exact': 2000}", + "Article matching query does not exist.", Article.objects.get, id__exact=2000, ) # To avoid dict-ordering related errors check only one lookup # in single assert. - six.assertRaisesRegex(self, + self.assertRaises( ObjectDoesNotExist, - ".*'pub_date__year': 2005.*", - Article.objects.get, - pub_date__year=2005, - pub_date__month=8, - ) - six.assertRaisesRegex(self, - ObjectDoesNotExist, - ".*'pub_date__month': 8.*", Article.objects.get, pub_date__year=2005, pub_date__month=8, @@ -111,8 +102,7 @@ class ModelTest(TestCase): six.assertRaisesRegex(self, ObjectDoesNotExist, - "Article matching query does not exist. Lookup parameters were " - "{'pub_date__week_day': 6}", + "Article matching query does not exist.", Article.objects.get, pub_date__week_day=6, ) @@ -442,7 +432,7 @@ class ModelTest(TestCase): Article.objects.all()[0:-5] except Exception as e: error = e - self.assertTrue(isinstance(error, AssertionError)) + self.assertIsInstance(error, AssertionError) self.assertEqual(str(error), "Negative indexing is not supported.") # An Article instance doesn't have access to the "objects" attribute. @@ -647,15 +637,15 @@ class ModelTest(TestCase): # Can't be instantiated with self.assertRaises(TypeError): EmptyQuerySet() - self.assertTrue(isinstance(Article.objects.none(), EmptyQuerySet)) + self.assertIsInstance(Article.objects.none(), EmptyQuerySet) def test_emptyqs_values(self): # test for #15959 Article.objects.create(headline='foo', pub_date=datetime.now()) with self.assertNumQueries(0): qs = Article.objects.none().values_list('pk') - self.assertTrue(isinstance(qs, EmptyQuerySet)) - self.assertTrue(isinstance(qs, ValuesListQuerySet)) + self.assertIsInstance(qs, EmptyQuerySet) + self.assertIsInstance(qs, ValuesListQuerySet) self.assertEqual(len(qs), 0) def test_emptyqs_customqs(self): @@ -670,7 +660,7 @@ class ModelTest(TestCase): qs = qs.none() with self.assertNumQueries(0): self.assertEqual(len(qs), 0) - self.assertTrue(isinstance(qs, EmptyQuerySet)) + self.assertIsInstance(qs, EmptyQuerySet) self.assertEqual(qs.do_something(), 'did something') def test_emptyqs_values_order(self): @@ -689,6 +679,12 @@ class ModelTest(TestCase): with self.assertNumQueries(0): self.assertEqual(len(Article.objects.none().distinct('headline', 'pub_date')), 0) + def test_ticket_20278(self): + sr = SelfRef.objects.create() + with self.assertRaises(ObjectDoesNotExist): + SelfRef.objects.get(selfref=sr) + + class ConcurrentSaveTests(TransactionTestCase): @skipUnlessDBFeature('test_db_allows_multiple_connections') def test_concurrent_delete_with_save(self): diff --git a/tests/cache/tests.py b/tests/cache/tests.py index 00c51638b7..da80c48058 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -28,8 +28,8 @@ from django.middleware.cache import (FetchFromCacheMiddleware, from django.template import Template from django.template.response import TemplateResponse from django.test import TestCase, TransactionTestCase, RequestFactory -from django.test.utils import override_settings, six -from django.utils import timezone, translation, unittest +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin +from django.utils import six, timezone, translation, unittest from django.utils.cache import (patch_vary_headers, get_cache_key, learn_cache_key, patch_cache_control, patch_response_headers) from django.utils.encoding import force_text @@ -441,6 +441,34 @@ class BaseCacheTests(object): self.assertEqual(self.cache.get('key3'), 'sausage') self.assertEqual(self.cache.get('key4'), 'lobster bisque') + def test_forever_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', None) + self.assertEqual(self.cache.get('key1'), 'eggs') + + self.cache.add('key2', 'ham', None) + self.assertEqual(self.cache.get('key2'), 'ham') + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, None) + self.assertEqual(self.cache.get('key3'), 'sausage') + self.assertEqual(self.cache.get('key4'), 'lobster bisque') + + def test_zero_timeout(self): + ''' + Passing in None into timeout results in a value that is cached forever + ''' + self.cache.set('key1', 'eggs', 0) + self.assertEqual(self.cache.get('key1'), None) + + self.cache.add('key2', 'ham', 0) + self.assertEqual(self.cache.get('key2'), None) + + self.cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 0) + self.assertEqual(self.cache.get('key3'), None) + self.assertEqual(self.cache.get('key4'), None) + def test_float_timeout(self): # Make sure a timeout given as a float doesn't crash anything. self.cache.set("key1", "spam", 100.2) @@ -482,13 +510,13 @@ class BaseCacheTests(object): # memcached does not allow whitespace or control characters in keys self.cache.set('key with spaces', 'value') self.assertEqual(len(w), 2) - self.assertTrue(isinstance(w[0].message, CacheKeyWarning)) + self.assertIsInstance(w[0].message, CacheKeyWarning) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # memcached limits key length to 250 self.cache.set('a' * 251, 'value') self.assertEqual(len(w), 1) - self.assertTrue(isinstance(w[0].message, CacheKeyWarning)) + self.assertIsInstance(w[0].message, CacheKeyWarning) finally: self.cache.key_func = old_func @@ -1069,10 +1097,10 @@ class GetCacheTests(unittest.TestCase): def test_simple(self): cache = get_cache('locmem://') from django.core.cache.backends.locmem import LocMemCache - self.assertTrue(isinstance(cache, LocMemCache)) + self.assertIsInstance(cache, LocMemCache) from django.core.cache import cache - self.assertTrue(isinstance(cache, get_cache('default').__class__)) + self.assertIsInstance(cache, get_cache('default').__class__) cache = get_cache( 'django.core.cache.backends.dummy.DummyCache', **{'TIMEOUT': 120}) @@ -1564,9 +1592,10 @@ def hello_world_view(request, value): }, }, ) -class CacheMiddlewareTest(TestCase): +class CacheMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TestCase): def setUp(self): + super(CacheMiddlewareTest, self).setUp() self.factory = RequestFactory() self.default_cache = get_cache('default') self.other_cache = get_cache('other') @@ -1574,6 +1603,7 @@ class CacheMiddlewareTest(TestCase): def tearDown(self): self.default_cache.clear() self.other_cache.clear() + super(CacheMiddlewareTest, self).tearDown() def test_constructor(self): """ diff --git a/tests/commands_sql/models.py b/tests/commands_sql/models.py index 089aa96f30..d8f372b403 100644 --- a/tests/commands_sql/models.py +++ b/tests/commands_sql/models.py @@ -2,6 +2,12 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible +@python_2_unicode_compatible +class Comment(models.Model): + pass + + @python_2_unicode_compatible class Book(models.Model): title = models.CharField(max_length=100, db_index=True) + comments = models.ManyToManyField(Comment) diff --git a/tests/commands_sql/tests.py b/tests/commands_sql/tests.py index 831e9a27c4..e083d3977a 100644 --- a/tests/commands_sql/tests.py +++ b/tests/commands_sql/tests.py @@ -12,41 +12,43 @@ from django.utils import six class SQLCommandsTestCase(TestCase): """Tests for several functions in django/core/management/sql.py""" + def count_ddl(self, output, cmd): + return len([o for o in output if o.startswith(cmd)]) + def test_sql_create(self): app = models.get_app('commands_sql') output = sql_create(app, no_style(), connections[DEFAULT_DB_ALIAS]) + create_tables = [o for o in output if o.startswith('CREATE TABLE')] + self.assertEqual(len(create_tables), 3) # Lower so that Oracle's upper case tbl names wont break - sql = output[0].lower() + sql = create_tables[-1].lower() six.assertRegex(self, sql, r'^create table .commands_sql_book.*') def test_sql_delete(self): app = models.get_app('commands_sql') output = sql_delete(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # Oracle produces DROP SEQUENCE and DROP TABLE for this command. - if connections[DEFAULT_DB_ALIAS].vendor == 'oracle': - sql = output[1].lower() - else: - sql = output[0].lower() - six.assertRegex(self, sql, r'^drop table .commands_sql_book.*') + drop_tables = [o for o in output if o.startswith('DROP TABLE')] + self.assertEqual(len(drop_tables), 3) + # Lower so that Oracle's upper case tbl names wont break + sql = drop_tables[-1].lower() + six.assertRegex(self, sql, r'^drop table .commands_sql_comment.*') def test_sql_indexes(self): app = models.get_app('commands_sql') output = sql_indexes(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # PostgreSQL creates two indexes - self.assertIn(len(output), [1, 2]) - self.assertTrue(output[0].startswith("CREATE INDEX")) + # PostgreSQL creates one additional index for CharField + self.assertIn(self.count_ddl(output, 'CREATE INDEX'), [3, 4]) def test_sql_destroy_indexes(self): app = models.get_app('commands_sql') output = sql_destroy_indexes(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # PostgreSQL creates two indexes - self.assertIn(len(output), [1, 2]) - self.assertTrue(output[0].startswith("DROP INDEX")) + # PostgreSQL creates one additional index for CharField + self.assertIn(self.count_ddl(output, 'DROP INDEX'), [3, 4]) def test_sql_all(self): app = models.get_app('commands_sql') output = sql_all(app, no_style(), connections[DEFAULT_DB_ALIAS]) - # PostgreSQL creates two indexes - self.assertIn(len(output), [2, 3]) - self.assertTrue(output[0].startswith('CREATE TABLE')) - self.assertTrue(output[1].startswith('CREATE INDEX')) + + self.assertEqual(self.count_ddl(output, 'CREATE TABLE'), 3) + # PostgreSQL creates one additional index for CharField + self.assertIn(self.count_ddl(output, 'CREATE INDEX'), [3, 4]) diff --git a/tests/comment_tests/tests/test_comment_form.py b/tests/comment_tests/tests/test_comment_form.py index 39ba57928d..a30f13a073 100644 --- a/tests/comment_tests/tests/test_comment_form.py +++ b/tests/comment_tests/tests/test_comment_form.py @@ -54,7 +54,7 @@ class CommentFormTests(CommentTestCase): def testGetCommentObject(self): f = self.testValidPost() c = f.get_comment_object() - self.assertTrue(isinstance(c, Comment)) + self.assertIsInstance(c, Comment) self.assertEqual(c.content_object, Article.objects.get(pk=1)) self.assertEqual(c.comment, "This is my comment") c.save() diff --git a/tests/comment_tests/tests/test_templatetags.py b/tests/comment_tests/tests/test_templatetags.py index 185f6de297..1971c21a58 100644 --- a/tests/comment_tests/tests/test_templatetags.py +++ b/tests/comment_tests/tests/test_templatetags.py @@ -32,7 +32,7 @@ class CommentTemplateTagTests(CommentTestCase): t = "{% load comments %}" + (tag or "{% get_comment_form for comment_tests.article a.id as form %}") ctx, out = self.render(t, a=Article.objects.get(pk=1)) self.assertEqual(out, "") - self.assertTrue(isinstance(ctx["form"], CommentForm)) + self.assertIsInstance(ctx["form"], CommentForm) def testGetCommentFormFromLiteral(self): self.testGetCommentForm("{% get_comment_form for comment_tests.article 1 as form %}") diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index 5300b2172a..841b24bb42 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import logging from django.conf import settings from django.core.context_processors import csrf @@ -78,18 +79,18 @@ class CsrfViewMiddlewareTest(TestCase): def _check_token_present(self, response, csrf_id=None): self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) - def test_process_view_token_too_long(self): - """ - Check that if the token is longer than expected, it is ignored and - a new token is created. - """ - req = self._get_GET_no_csrf_cookie_request() - req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 - CsrfViewMiddleware().process_view(req, token_view, (), {}) - resp = token_view(req) - resp2 = CsrfViewMiddleware().process_response(req, resp) - csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) - self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) + def test_process_view_token_too_long(self): + """ + Check that if the token is longer than expected, it is ignored and + a new token is created. + """ + req = self._get_GET_no_csrf_cookie_request() + req.COOKIES[settings.CSRF_COOKIE_NAME] = 'x' * 10000000 + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertEqual(len(csrf_cookie.value), CSRF_KEY_LENGTH) def test_process_response_get_token_used(self): """ @@ -283,6 +284,19 @@ class CsrfViewMiddlewareTest(TestCase): self.assertNotEqual(None, req2) self.assertEqual(403, req2.status_code) + @override_settings(ALLOWED_HOSTS=['www.example.com']) + def test_https_malformed_referer(self): + """ + Test that a POST HTTPS request with a bad referer is rejected + """ + req = self._get_POST_request_with_token() + req._is_secure_override = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'http://http://www.example.com/' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertNotEqual(None, req2) + self.assertEqual(403, req2.status_code) + @override_settings(ALLOWED_HOSTS=['www.example.com']) def test_https_good_referer(self): """ @@ -340,3 +354,29 @@ class CsrfViewMiddlewareTest(TestCase): resp2 = CsrfViewMiddleware().process_response(req, resp) self.assertTrue(resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)) self.assertTrue('Cookie' in resp2.get('Vary','')) + + def test_ensures_csrf_cookie_no_logging(self): + """ + Tests that ensure_csrf_cookie doesn't log warnings. See #19436. + """ + @ensure_csrf_cookie + def view(request): + # Doesn't insert a token or anything + return HttpResponse(content="") + + class TestHandler(logging.Handler): + def emit(self, record): + raise Exception("This shouldn't have happened!") + + logger = logging.getLogger('django.request') + test_handler = TestHandler() + old_log_level = logger.level + try: + logger.addHandler(test_handler) + logger.setLevel(logging.WARNING) + + req = self._get_GET_no_csrf_cookie_request() + resp = view(req) + finally: + logger.removeHandler(test_handler) + logger.setLevel(old_log_level) diff --git a/tests/custom_managers/tests.py b/tests/custom_managers/tests.py index 294920de2b..4fe79fe3fb 100644 --- a/tests/custom_managers/tests.py +++ b/tests/custom_managers/tests.py @@ -19,7 +19,7 @@ class CustomManagerTests(TestCase): ) # The RelatedManager used on the 'books' descriptor extends the default # manager - self.assertTrue(isinstance(p2.books, PublishedBookManager)) + self.assertIsInstance(p2.books, PublishedBookManager) b1 = Book.published_objects.create( title="How to program", author="Rodney Dangerfield", is_published=True @@ -34,7 +34,7 @@ class CustomManagerTests(TestCase): # The RelatedManager used on the 'authors' descriptor extends the # default manager - self.assertTrue(isinstance(b2.authors, PersonManager)) + self.assertIsInstance(b2.authors, PersonManager) self.assertQuerysetEqual( Book.published_objects.all(), [ diff --git a/tests/datatypes/tests.py b/tests/datatypes/tests.py index f0ec5f3c0a..b6b52dedf2 100644 --- a/tests/datatypes/tests.py +++ b/tests/datatypes/tests.py @@ -74,7 +74,7 @@ class DataTypesTestCase(TestCase): database should be unicode.""" d = Donut.objects.create(name='Jelly Donut', review='Outstanding') newd = Donut.objects.get(id=d.id) - self.assertTrue(isinstance(newd.review, six.text_type)) + self.assertIsInstance(newd.review, six.text_type) @skipIfDBFeature('supports_timezones') def test_error_on_timezone(self): @@ -90,7 +90,7 @@ class DataTypesTestCase(TestCase): a Python datetime.date, not a datetime.datetime""" b = RumBaba.objects.create() # Verify we didn't break DateTimeField behavior - self.assertTrue(isinstance(b.baked_timestamp, datetime.datetime)) + self.assertIsInstance(b.baked_timestamp, datetime.datetime) # We need to test this this way because datetime.datetime inherits # from datetime.date: - self.assertTrue(isinstance(b.baked_date, datetime.date) and not isinstance(b.baked_date, datetime.datetime)) + self.assertIsInstance(b.baked_date, datetime.date) and not isinstance(b.baked_date, datetime.datetime) diff --git a/tests/decorators/tests.py b/tests/decorators/tests.py index 8a3f340e9f..05016be231 100644 --- a/tests/decorators/tests.py +++ b/tests/decorators/tests.py @@ -126,15 +126,15 @@ class DecoratorsTest(TestCase): my_safe_view = require_safe(my_view) request = HttpRequest() request.method = 'GET' - self.assertTrue(isinstance(my_safe_view(request), HttpResponse)) + self.assertIsInstance(my_safe_view(request), HttpResponse) request.method = 'HEAD' - self.assertTrue(isinstance(my_safe_view(request), HttpResponse)) + self.assertIsInstance(my_safe_view(request), HttpResponse) request.method = 'POST' - self.assertTrue(isinstance(my_safe_view(request), HttpResponseNotAllowed)) + self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) request.method = 'PUT' - self.assertTrue(isinstance(my_safe_view(request), HttpResponseNotAllowed)) + self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) request.method = 'DELETE' - self.assertTrue(isinstance(my_safe_view(request), HttpResponseNotAllowed)) + self.assertIsInstance(my_safe_view(request), HttpResponseNotAllowed) # For testing method_decorator, a decorator that assumes a single argument. diff --git a/tests/defaultfilters/tests.py b/tests/defaultfilters/tests.py index 21734faf95..d0009c6e66 100644 --- a/tests/defaultfilters/tests.py +++ b/tests/defaultfilters/tests.py @@ -11,6 +11,8 @@ from django.utils import unittest, translation from django.utils.safestring import SafeData from django.utils.encoding import python_2_unicode_compatible +from i18n import TransRealMixin + class DefaultFiltersTests(TestCase): @@ -306,13 +308,13 @@ class DefaultFiltersTests(TestCase): self.assertEqual(urlize('(Go to http://www.example.com/foo.)'), '(Go to <a href="http://www.example.com/foo" rel="nofollow">http://www.example.com/foo</a>.)') - # Check urlize doesn't crash when square bracket is appended to url (#19070) + # Check urlize handles brackets properly (#19070) self.assertEqual(urlize('[see www.example.com]'), '[see <a href="http://www.example.com" rel="nofollow">www.example.com</a>]' ) - - # Check urlize doesn't crash when square bracket is prepended to url (#19070) self.assertEqual(urlize('see test[at[example.com'), 'see <a href="http://test[at[example.com" rel="nofollow">test[at[example.com</a>' ) + self.assertEqual(urlize('[http://168.192.0.1](http://168.192.0.1)'), + '[<a href="http://168.192.0.1](http://168.192.0.1)" rel="nofollow">http://168.192.0.1](http://168.192.0.1)</a>') # Check urlize works with IPv4/IPv6 addresses self.assertEqual(urlize('http://192.168.0.15/api/9'), @@ -360,7 +362,7 @@ class DefaultFiltersTests(TestCase): escaped = force_escape('<some html & special characters > here') self.assertEqual( escaped, '<some html & special characters > here') - self.assertTrue(isinstance(escaped, SafeData)) + self.assertIsInstance(escaped, SafeData) self.assertEqual( force_escape('<some html & special characters > here ĐÅ€£'), '<some html & special characters > here'\ @@ -527,24 +529,26 @@ class DefaultFiltersTests(TestCase): def test_timesince(self): # real testing is done in timesince.py, where we can provide our own 'now' + # NOTE: \xa0 avoids wrapping between value and unit self.assertEqual( timesince_filter(datetime.datetime.now() - datetime.timedelta(1)), - '1 day') + '1\xa0day') self.assertEqual( timesince_filter(datetime.datetime(2005, 12, 29), datetime.datetime(2005, 12, 30)), - '1 day') + '1\xa0day') def test_timeuntil(self): + # NOTE: \xa0 avoids wrapping between value and unit self.assertEqual( timeuntil_filter(datetime.datetime.now() + datetime.timedelta(1, 1)), - '1 day') + '1\xa0day') self.assertEqual( timeuntil_filter(datetime.datetime(2005, 12, 30), datetime.datetime(2005, 12, 29)), - '1 day') + '1\xa0day') def test_default(self): self.assertEqual(default("val", "default"), 'val') @@ -574,43 +578,23 @@ class DefaultFiltersTests(TestCase): 'get out of town') def test_filesizeformat(self): - self.assertEqual(filesizeformat(1023), '1023 bytes') - self.assertEqual(filesizeformat(1024), '1.0 KB') - self.assertEqual(filesizeformat(10*1024), '10.0 KB') - self.assertEqual(filesizeformat(1024*1024-1), '1024.0 KB') - self.assertEqual(filesizeformat(1024*1024), '1.0 MB') - self.assertEqual(filesizeformat(1024*1024*50), '50.0 MB') - self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0 MB') - self.assertEqual(filesizeformat(1024*1024*1024), '1.0 GB') - self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0 TB') - self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0 PB') + # NOTE: \xa0 avoids wrapping between value and unit + self.assertEqual(filesizeformat(1023), '1023\xa0bytes') + self.assertEqual(filesizeformat(1024), '1.0\xa0KB') + self.assertEqual(filesizeformat(10*1024), '10.0\xa0KB') + self.assertEqual(filesizeformat(1024*1024-1), '1024.0\xa0KB') + self.assertEqual(filesizeformat(1024*1024), '1.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*50), '50.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024-1), '1024.0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024), '1.0\xa0GB') + self.assertEqual(filesizeformat(1024*1024*1024*1024), '1.0\xa0TB') + self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), '1.0\xa0PB') self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), - '2000.0 PB') - self.assertEqual(filesizeformat(complex(1,-1)), '0 bytes') - self.assertEqual(filesizeformat(""), '0 bytes') + '2000.0\xa0PB') + self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0bytes') + self.assertEqual(filesizeformat(""), '0\xa0bytes') self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), - '0 bytes') - - def test_localized_filesizeformat(self): - with self.settings(USE_L10N=True): - with translation.override('de', deactivate=True): - self.assertEqual(filesizeformat(1023), '1023 Bytes') - self.assertEqual(filesizeformat(1024), '1,0 KB') - self.assertEqual(filesizeformat(10*1024), '10,0 KB') - self.assertEqual(filesizeformat(1024*1024-1), '1024,0 KB') - self.assertEqual(filesizeformat(1024*1024), '1,0 MB') - self.assertEqual(filesizeformat(1024*1024*50), '50,0 MB') - self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0 MB') - self.assertEqual(filesizeformat(1024*1024*1024), '1,0 GB') - self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0 TB') - self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), - '1,0 PB') - self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), - '2000,0 PB') - self.assertEqual(filesizeformat(complex(1,-1)), '0 Bytes') - self.assertEqual(filesizeformat(""), '0 Bytes') - self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), - '0 Bytes') + '0\xa0bytes') def test_pluralize(self): self.assertEqual(pluralize(1), '') @@ -656,3 +640,27 @@ class DefaultFiltersTests(TestCase): self.assertEqual(removetags(123, 'a'), '123') self.assertEqual(striptags(123), '123') + +class DefaultFiltersI18NTests(TransRealMixin, TestCase): + + def test_localized_filesizeformat(self): + # NOTE: \xa0 avoids wrapping between value and unit + with self.settings(USE_L10N=True): + with translation.override('de', deactivate=True): + self.assertEqual(filesizeformat(1023), '1023\xa0Bytes') + self.assertEqual(filesizeformat(1024), '1,0\xa0KB') + self.assertEqual(filesizeformat(10*1024), '10,0\xa0KB') + self.assertEqual(filesizeformat(1024*1024-1), '1024,0\xa0KB') + self.assertEqual(filesizeformat(1024*1024), '1,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*50), '50,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024-1), '1024,0\xa0MB') + self.assertEqual(filesizeformat(1024*1024*1024), '1,0\xa0GB') + self.assertEqual(filesizeformat(1024*1024*1024*1024), '1,0\xa0TB') + self.assertEqual(filesizeformat(1024*1024*1024*1024*1024), + '1,0\xa0PB') + self.assertEqual(filesizeformat(1024*1024*1024*1024*1024*2000), + '2000,0\xa0PB') + self.assertEqual(filesizeformat(complex(1,-1)), '0\xa0Bytes') + self.assertEqual(filesizeformat(""), '0\xa0Bytes') + self.assertEqual(filesizeformat("\N{GREEK SMALL LETTER ALPHA}"), + '0\xa0Bytes') diff --git a/tests/defer_regress/models.py b/tests/defer_regress/models.py index 7528e3b2c9..0170221cb9 100644 --- a/tests/defer_regress/models.py +++ b/tests/defer_regress/models.py @@ -62,3 +62,22 @@ class OneToOneItem(models.Model): class ItemAndSimpleItem(models.Model): item = models.ForeignKey(Item) simple = models.ForeignKey(SimpleItem) + +class Profile(models.Model): + profile1 = models.CharField(max_length=1000, default='profile1') + +class Location(models.Model): + location1 = models.CharField(max_length=1000, default='location1') + +class Item(models.Model): + pass + +class Request(models.Model): + profile = models.ForeignKey(Profile, null=True, blank=True) + location = models.ForeignKey(Location) + items = models.ManyToManyField(Item) + + request1 = models.CharField(default='request1', max_length=1000) + request2 = models.CharField(default='request2', max_length=1000) + request3 = models.CharField(default='request3', max_length=1000) + request4 = models.CharField(default='request4', max_length=1000) diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py index d4d722035f..ad2546794c 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -8,8 +8,9 @@ from django.db.models import Count from django.db.models.loading import cache from django.test import TestCase -from .models import (ResolveThis, Item, RelatedItem, Child, Leaf, Proxy, - SimpleItem, Feature, ItemAndSimpleItem, OneToOneItem, SpecialFeature) +from .models import ( + ResolveThis, Item, RelatedItem, Child, Leaf, Proxy, SimpleItem, Feature, + ItemAndSimpleItem, OneToOneItem, SpecialFeature, Location, Request) class DeferRegressionTest(TestCase): @@ -76,7 +77,9 @@ class DeferRegressionTest(TestCase): self.assertEqual(results[0].child.name, "c1") self.assertEqual(results[0].second_child.name, "c2") - results = Leaf.objects.only("name", "child", "second_child", "child__name", "second_child__name").select_related() + results = Leaf.objects.only( + "name", "child", "second_child", "child__name", "second_child__name" + ).select_related() self.assertEqual(results[0].child.name, "c1") self.assertEqual(results[0].second_child.name, "c2") @@ -98,29 +101,36 @@ class DeferRegressionTest(TestCase): i2 = s["item"] self.assertFalse(i2._deferred) + # Regression for #16409 - make sure defer() and only() work with annotate() + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).defer('name')), + list) + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).only('name')), + list) + + def test_ticket_11936(self): # Regression for #11936 - loading.get_models should not return deferred # models by default. - klasses = sorted( - cache.get_models(cache.get_app("defer_regress")), - key=lambda klass: klass.__name__ - ) - self.assertEqual( - klasses, [ - Child, - Feature, - Item, - ItemAndSimpleItem, - Leaf, - OneToOneItem, - Proxy, - RelatedItem, - ResolveThis, - SimpleItem, - SpecialFeature, - ] + # Run a couple of defer queries so that app-cache must contain some + # deferred classes. It might contain a lot more classes depending on + # the order the tests are ran. + list(Item.objects.defer("name")) + list(Child.objects.defer("value")) + klasses = set( + map( + attrgetter("__name__"), + cache.get_models(cache.get_app("defer_regress")) + ) ) + self.assertIn("Child", klasses) + self.assertIn("Item", klasses) + self.assertNotIn("Child_Deferred_value", klasses) + self.assertNotIn("Item_Deferred_name", klasses) + self.assertFalse(any( + k._deferred for k in cache.get_models(cache.get_app("defer_regress")))) - klasses = sorted( + klasses_with_deferred = set( map( attrgetter("__name__"), cache.get_models( @@ -128,40 +138,22 @@ class DeferRegressionTest(TestCase): ), ) ) - # FIXME: This is dependent on the order in which tests are run -- - # this test case has to be the first, otherwise a LOT more classes - # appear. - self.assertEqual( - klasses, [ - "Child", - "Child_Deferred_value", - "Feature", - "Item", - "ItemAndSimpleItem", - "Item_Deferred_name", - "Item_Deferred_name_other_value_text", - "Item_Deferred_name_other_value_value", - "Item_Deferred_other_value_text_value", - "Item_Deferred_text_value", - "Leaf", - "Leaf_Deferred_child_id_second_child_id_value", - "Leaf_Deferred_name_value", - "Leaf_Deferred_second_child_id_value", - "Leaf_Deferred_value", - "OneToOneItem", - "Proxy", - "RelatedItem", - "RelatedItem_Deferred_", - "RelatedItem_Deferred_item_id", - "ResolveThis", - "SimpleItem", - "SpecialFeature", - ] + self.assertIn("Child", klasses_with_deferred) + self.assertIn("Item", klasses_with_deferred) + self.assertIn("Child_Deferred_value", klasses_with_deferred) + self.assertIn("Item_Deferred_name", klasses_with_deferred) + self.assertTrue(any( + k._deferred for k in cache.get_models( + cache.get_app("defer_regress"), include_deferred=True)) ) # Regression for #16409 - make sure defer() and only() work with annotate() - self.assertIsInstance(list(SimpleItem.objects.annotate(Count('feature')).defer('name')), list) - self.assertIsInstance(list(SimpleItem.objects.annotate(Count('feature')).only('name')), list) + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).defer('name')), + list) + self.assertIsInstance( + list(SimpleItem.objects.annotate(Count('feature')).only('name')), + list) def test_only_and_defer_usage_on_proxy_models(self): # Regression for #15790 - only() broken for proxy models @@ -179,7 +171,7 @@ class DeferRegressionTest(TestCase): self.assertEqual(dp.value, proxy.value, msg=msg) def test_resolve_columns(self): - rt = ResolveThis.objects.create(num=5.0, name='Foobar') + ResolveThis.objects.create(num=5.0, name='Foobar') qs = ResolveThis.objects.defer('num') self.assertEqual(1, qs.count()) self.assertEqual('Foobar', qs[0].name) @@ -215,7 +207,7 @@ class DeferRegressionTest(TestCase): item1 = Item.objects.create(name="first", value=47) item2 = Item.objects.create(name="second", value=42) simple = SimpleItem.objects.create(name="simple", value="23") - related = ItemAndSimpleItem.objects.create(item=item1, simple=simple) + ItemAndSimpleItem.objects.create(item=item1, simple=simple) obj = ItemAndSimpleItem.objects.defer('item').select_related('simple').get() self.assertEqual(obj.item, item1) @@ -242,7 +234,23 @@ class DeferRegressionTest(TestCase): def test_deferred_class_factory(self): from django.db.models.query_utils import deferred_class_factory - new_class = deferred_class_factory(Item, + new_class = deferred_class_factory( + Item, ('this_is_some_very_long_attribute_name_so_modelname_truncation_is_triggered',)) - self.assertEqual(new_class.__name__, + self.assertEqual( + new_class.__name__, 'Item_Deferred_this_is_some_very_long_attribute_nac34b1f495507dad6b02e2cb235c875e') + +class DeferAnnotateSelectRelatedTest(TestCase): + def test_defer_annotate_select_related(self): + location = Location.objects.create() + Request.objects.create(location=location) + self.assertIsInstance(list(Request.objects + .annotate(Count('items')).select_related('profile', 'location') + .only('profile', 'location')), list) + self.assertIsInstance(list(Request.objects + .annotate(Count('items')).select_related('profile', 'location') + .only('profile__profile1', 'location__location1')), list) + self.assertIsInstance(list(Request.objects + .annotate(Count('items')).select_related('profile', 'location') + .defer('request1', 'request2', 'request3', 'request4')), list) diff --git a/tests/dispatch/tests/test_dispatcher.py b/tests/dispatch/tests/test_dispatcher.py index 5f8f92acaf..a1d4c7e176 100644 --- a/tests/dispatch/tests/test_dispatcher.py +++ b/tests/dispatch/tests/test_dispatcher.py @@ -108,7 +108,7 @@ class DispatcherTests(unittest.TestCase): a_signal.connect(fails) result = a_signal.send_robust(sender=self, val="test") err = result[0][1] - self.assertTrue(isinstance(err, ValueError)) + self.assertIsInstance(err, ValueError) self.assertEqual(err.args, ('this',)) a_signal.disconnect(fails) self._testIsClean(a_signal) diff --git a/tests/field_defaults/tests.py b/tests/field_defaults/tests.py index 5d9b45610e..69dabb5168 100644 --- a/tests/field_defaults/tests.py +++ b/tests/field_defaults/tests.py @@ -14,6 +14,6 @@ class DefaultTests(TestCase): now = datetime.now() a.save() - self.assertTrue(isinstance(a.id, six.integer_types)) + self.assertIsInstance(a.id, six.integer_types) self.assertEqual(a.headline, "Default headline") self.assertTrue((now - a.pub_date).seconds < 5) diff --git a/tests/field_subclassing/tests.py b/tests/field_subclassing/tests.py index 9331ff2f3f..4945cff1bf 100644 --- a/tests/field_subclassing/tests.py +++ b/tests/field_subclassing/tests.py @@ -11,21 +11,21 @@ class CustomField(TestCase): def test_defer(self): d = DataModel.objects.create(data=[1, 2, 3]) - self.assertTrue(isinstance(d.data, list)) + self.assertIsInstance(d.data, list) d = DataModel.objects.get(pk=d.pk) - self.assertTrue(isinstance(d.data, list)) + self.assertIsInstance(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.assertIsInstance(d.data, list) self.assertEqual(d.data, [1, 2, 3]) # Refetch for save d = DataModel.objects.defer("data").get(pk=d.pk) d.save() d = DataModel.objects.get(pk=d.pk) - self.assertTrue(isinstance(d.data, list)) + self.assertIsInstance(d.data, list) self.assertEqual(d.data, [1, 2, 3]) def test_custom_field(self): @@ -44,7 +44,7 @@ class CustomField(TestCase): # The data loads back from the database correctly and 'data' has the # right type. m1 = MyModel.objects.get(pk=m.pk) - self.assertTrue(isinstance(m1.data, Small)) + self.assertIsInstance(m1.data, Small) self.assertEqual(str(m1.data), "12") # We can do normal filtering on the custom field (and will get an error diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index e4b71dba82..406a4d79b6 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -588,11 +588,11 @@ class ContentFileTestCase(unittest.TestCase): Test that ContentFile can accept both bytes and unicode and that the retrieved content is of the same type. """ - self.assertTrue(isinstance(ContentFile(b"content").read(), bytes)) + self.assertIsInstance(ContentFile(b"content").read(), bytes) if six.PY3: - self.assertTrue(isinstance(ContentFile("español").read(), six.text_type)) + self.assertIsInstance(ContentFile("español").read(), six.text_type) else: - self.assertTrue(isinstance(ContentFile("español").read(), bytes)) + self.assertIsInstance(ContentFile("español").read(), bytes) def test_content_saving(self): """ diff --git a/tests/fixtures/tests.py b/tests/fixtures/tests.py index 103612198e..1b4d6eeeef 100644 --- a/tests/fixtures/tests.py +++ b/tests/fixtures/tests.py @@ -1,5 +1,7 @@ from __future__ import absolute_import +import warnings + from django.contrib.sites.models import Site from django.core import management from django.db import connection, IntegrityError @@ -25,14 +27,15 @@ class TestCaseFixtureLoadingTests(TestCase): class DumpDataAssertMixin(object): def _dumpdata_assert(self, args, output, format='json', natural_keys=False, - use_base_manager=False, exclude_list=[]): + use_base_manager=False, exclude_list=[], primary_keys=''): new_io = six.StringIO() management.call_command('dumpdata', *args, **{'format': format, 'stdout': new_io, 'stderr': new_io, 'use_natural_keys': natural_keys, 'use_base_manager': use_base_manager, - 'exclude': exclude_list}) + 'exclude': exclude_list, + 'primary_keys': primary_keys}) command_output = new_io.getvalue().strip() if format == "json": self.assertJSONEqual(command_output, output) @@ -137,8 +140,19 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): '<Book: Music for all ages by Artist formerly known as "Prince" and Django Reinhardt>' ]) - # Load a fixture that doesn't exist - management.call_command('loaddata', 'unknown.json', verbosity=0, commit=False) + # Loading a fixture that doesn't exist emits a warning + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + management.call_command('loaddata', 'unknown.json', verbosity=0, + commit=False) + self.assertEqual(len(w), 1) + self.assertTrue(w[0].message, "No fixture named 'unknown' found.") + + # An attempt to load a nonexistent 'initial_data' fixture isn't an error + with warnings.catch_warnings(record=True) as w: + management.call_command('loaddata', 'initial_data.json', verbosity=0, + commit=False) + self.assertEqual(len(w), 0) # object list is unaffected self.assertQuerysetEqual(Article.objects.all(), [ @@ -211,6 +225,45 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): # even those normally filtered by the manager self._dumpdata_assert(['fixtures.Spy'], '[{"pk": %d, "model": "fixtures.spy", "fields": {"cover_blown": true}}, {"pk": %d, "model": "fixtures.spy", "fields": {"cover_blown": false}}]' % (spy2.pk, spy1.pk), use_base_manager=True) + def test_dumpdata_with_pks(self): + management.call_command('loaddata', 'fixture1.json', verbosity=0, commit=False) + management.call_command('loaddata', 'fixture2.json', verbosity=0, commit=False) + 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": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + 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"}}]', + primary_keys='2' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + ['fixtures'], + '[{"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": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + '', + '[{"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": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + + with six.assertRaisesRegex(self, management.CommandError, + "You can only use --pks option with one model"): + self._dumpdata_assert( + ['fixtures.Article', 'fixtures.category'], + '[{"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": "Copyright is fine the way it is", "pub_date": "2006-06-16T14:00:00"}}]', + primary_keys='2,3' + ) + def test_compress_format_loading(self): # Load fixture 4 (compressed), using format specification management.call_command('loaddata', 'fixture4.json', verbosity=0, commit=False) @@ -273,10 +326,11 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): def test_unmatched_identifier_loading(self): # Try to load db fixture 3. This won't load because the database identifier doesn't match - management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) - self.assertQuerysetEqual(Article.objects.all(), []) + with warnings.catch_warnings(record=True): + management.call_command('loaddata', 'db_fixture_3', verbosity=0, commit=False) - management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) + with warnings.catch_warnings(record=True): + management.call_command('loaddata', 'db_fixture_3', verbosity=0, using='default', commit=False) self.assertQuerysetEqual(Article.objects.all(), []) def test_output_formats(self): diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index c250f647ce..e0a35e300d 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import warnings + from django.core import management from django.db import transaction from django.test import TestCase, TransactionTestCase @@ -100,7 +102,12 @@ class FixtureTestCase(TestCase): ) # Load a fixture that doesn't exist - management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + management.call_command("loaddata", "unknown.json", verbosity=0, commit=False) + self.assertEqual(len(w), 1) + self.assertTrue(w[0].message, "No fixture named 'unknown' found.") + self.assertQuerysetEqual( Article.objects.all(), [ "Django conquers world!", diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index 02e923e386..52526ec338 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals import os import re +import warnings from django.core.serializers.base import DeserializationError from django.core import management @@ -441,14 +442,15 @@ class TestFixtures(TestCase): def test_loaddata_not_existant_fixture_file(self): stdout_output = StringIO() - management.call_command( - 'loaddata', - 'this_fixture_doesnt_exist', - verbosity=2, - commit=False, - stdout=stdout_output, - ) - self.assertTrue("No xml fixture 'this_fixture_doesnt_exist' in" in + with warnings.catch_warnings(record=True): + management.call_command( + 'loaddata', + 'this_fixture_doesnt_exist', + verbosity=2, + commit=False, + stdout=stdout_output, + ) + self.assertTrue("No fixture 'this_fixture_doesnt_exist' in" in force_text(stdout_output.getvalue())) diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py index 55dd6a0f47..69636ee49b 100644 --- a/tests/foreign_object/tests.py +++ b/tests/foreign_object/tests.py @@ -316,6 +316,11 @@ class MultiColumnFKTests(TestCase): list(Article.objects.filter(active_translation__abstract=None)), [a1, a2]) + def test_foreign_key_raises_informative_does_not_exist(self): + referrer = ArticleTranslation() + with self.assertRaisesMessage(Article.DoesNotExist, 'ArticleTranslation has no article'): + referrer.article + class FormsTests(TestCase): # ForeignObjects should not have any form fields, currently the user needs # to manually deal with the foreignobject relation. diff --git a/tests/forms_tests/tests/test_extra.py b/tests/forms_tests/tests/test_extra.py index a83cdfc05f..ea0f063c30 100644 --- a/tests/forms_tests/tests/test_extra.py +++ b/tests/forms_tests/tests/test_extra.py @@ -569,6 +569,14 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): f = GenericIPAddressField(unpack_ipv4=True) self.assertEqual(f.clean(' ::ffff:0a0a:0a0a'), '10.10.10.10') + def test_slugfield_normalization(self): + f = SlugField() + self.assertEqual(f.clean(' aa-bb-cc '), 'aa-bb-cc') + + def test_urlfield_normalization(self): + f = URLField() + self.assertEqual(f.clean('http://example.com/ '), 'http://example.com/') + def test_smart_text(self): class Test: if six.PY3: diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py index 7516de29b4..47c637befa 100644 --- a/tests/forms_tests/tests/test_fields.py +++ b/tests/forms_tests/tests/test_fields.py @@ -125,6 +125,15 @@ class FieldsTests(SimpleTestCase): self.assertEqual(f.max_length, None) self.assertEqual(f.min_length, 10) + def test_charfield_length_not_int(self): + """ + Ensure that setting min_length or max_length to something that is not a + number returns an exception. + """ + self.assertRaises(ValueError, CharField, min_length='a') + self.assertRaises(ValueError, CharField, max_length='a') + self.assertRaises(ValueError, CharField, 'a') + def test_charfield_widget_attrs(self): """ Ensure that CharField.widget_attrs() always returns a dictionary. diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index 45e62a492c..1a3fb44a66 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -1398,7 +1398,7 @@ class FormsTestCase(TestCase): birthday = DateField() def add_prefix(self, field_name): - return self.prefix and '%s-prefix-%s' % (self.prefix, field_name) or field_name + return '%s-prefix-%s' % (self.prefix, field_name) if self.prefix else field_name p = Person(prefix='foo') self.assertHTMLEqual(p.as_ul(), """<li><label for="id_foo-prefix-first_name">First name:</label> <input type="text" name="foo-prefix-first_name" id="id_foo-prefix-first_name" /></li> @@ -1846,3 +1846,20 @@ class FormsTestCase(TestCase): self.assertHTMLEqual(boundfield.label_tag(), 'Field') self.assertHTMLEqual(boundfield.label_tag('Custom&'), 'Custom&') + + def test_boundfield_label_tag_custom_widget_id_for_label(self): + class CustomIdForLabelTextInput(TextInput): + def id_for_label(self, id): + return 'custom_' + id + + class EmptyIdForLabelTextInput(TextInput): + def id_for_label(self, id): + return None + + class SomeForm(Form): + custom = CharField(widget=CustomIdForLabelTextInput) + empty = CharField(widget=EmptyIdForLabelTextInput) + + form = SomeForm() + self.assertHTMLEqual(form['custom'].label_tag(), '<label for="custom_id_custom">Custom</label>') + self.assertHTMLEqual(form['empty'].label_tag(), '<label>Empty</label>') diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 4ac3c5ecf1..1e9e7db30c 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +import datetime + from django.forms import (CharField, DateField, FileField, Form, IntegerField, - ValidationError, formsets) + SplitDateTimeField, ValidationError, formsets) from django.forms.formsets import BaseFormSet, formset_factory from django.forms.util import ErrorList from django.test import TestCase @@ -45,6 +47,13 @@ FavoriteDrinksFormSet = formset_factory(FavoriteDrinkForm, formset=BaseFavoriteDrinksFormSet, extra=3) +# Used in ``test_formset_splitdatetimefield``. +class SplitDateTimeForm(Form): + when = SplitDateTimeField(initial=datetime.datetime.now) + +SplitDateTimeFormSet = formset_factory(SplitDateTimeForm) + + class FormsFormsetTestCase(TestCase): def test_basic_formset(self): # A FormSet constructor takes the same arguments as Form. Let's create a FormSet @@ -882,6 +891,19 @@ class FormsFormsetTestCase(TestCase): self.assertEqual(len(formset.forms), 0) self.assertTrue(formset) + def test_formset_splitdatetimefield(self): + """ + Formset should also work with SplitDateTimeField(initial=datetime.datetime.now). + Regression test for #18709. + """ + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-0-when_0': '1904-06-16', + 'form-0-when_1': '15:51:33', + } + formset = SplitDateTimeFormSet(data) + self.assertTrue(formset.is_valid()) def test_formset_error_class(self): # Regression tests for #16479 -- formsets form use ErrorList instead of supplied error_class @@ -972,6 +994,38 @@ class FormsFormsetTestCase(TestCase): finally: formsets.DEFAULT_MAX_NUM = _old_DEFAULT_MAX_NUM + def test_non_form_errors_run_full_clean(self): + # Regression test for #11160 + # If non_form_errors() is called without calling is_valid() first, + # it should ensure that full_clean() is called. + class BaseCustomFormSet(BaseFormSet): + def clean(self): + raise ValidationError("This is a non-form error") + + ChoiceFormSet = formset_factory(Choice, formset=BaseCustomFormSet) + formset = ChoiceFormSet(data, auto_id=False, prefix='choices') + self.assertIsInstance(formset.non_form_errors(), ErrorList) + self.assertEqual(list(formset.non_form_errors()), + ['This is a non-form error']) + + def test_validate_max_ignores_forms_marked_for_deletion(self): + class CheckForm(Form): + field = IntegerField() + + data = { + 'check-TOTAL_FORMS': '2', + 'check-INITIAL_FORMS': '0', + 'check-MAX_NUM_FORMS': '1', + 'check-0-field': '200', + 'check-0-DELETE': '', + 'check-1-field': '50', + 'check-1-DELETE': 'on', + } + CheckFormSet = formset_factory(CheckForm, max_num=1, validate_max=True, + can_delete=True) + formset = CheckFormSet(data, prefix='check') + self.assertTrue(formset.is_valid()) + data = { 'choices-TOTAL_FORMS': '1', # the number of forms rendered diff --git a/tests/forms_tests/tests/test_regressions.py b/tests/forms_tests/tests/test_regressions.py index 74509a0f1a..ea138d32d5 100644 --- a/tests/forms_tests/tests/test_regressions.py +++ b/tests/forms_tests/tests/test_regressions.py @@ -8,9 +8,10 @@ from django.test import TestCase from django.utils.translation import ugettext_lazy, override from forms_tests.models import Cheese +from i18n import TransRealMixin -class FormsRegressionsTestCase(TestCase): +class FormsRegressionsTestCase(TransRealMixin, TestCase): def test_class(self): # Tests to prevent against recurrences of earlier bugs. extra_attrs = {'class': 'special'} diff --git a/tests/forms_tests/tests/tests.py b/tests/forms_tests/tests/tests.py index deda4822b8..2616ddaf7d 100644 --- a/tests/forms_tests/tests/tests.py +++ b/tests/forms_tests/tests/tests.py @@ -5,7 +5,7 @@ import datetime from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models -from django.forms import Form, ModelForm, FileField, ModelChoiceField +from django.forms import Form, ModelForm, FileField, ModelChoiceField, CharField from django.forms.models import ModelFormMetaclass from django.test import TestCase from django.utils import six @@ -26,6 +26,14 @@ class OptionalMultiChoiceModelForm(ModelForm): fields = '__all__' +class ChoiceFieldExclusionForm(ModelForm): + multi_choice = CharField(max_length=50) + + class Meta: + exclude = ['multi_choice'] + model = ChoiceFieldModel + + class FileForm(Form): file1 = FileField() @@ -52,9 +60,9 @@ class TestTicket14567(TestCase): form = OptionalMultiChoiceModelForm({'multi_choice_optional': '', 'multi_choice': [option.pk]}) self.assertTrue(form.is_valid()) # Check that the empty value is a QuerySet - self.assertTrue(isinstance(form.cleaned_data['multi_choice_optional'], models.query.QuerySet)) + self.assertIsInstance(form.cleaned_data['multi_choice_optional'], models.query.QuerySet) # While we're at it, test whether a QuerySet is returned if there *is* a value. - self.assertTrue(isinstance(form.cleaned_data['multi_choice'], models.query.QuerySet)) + self.assertIsInstance(form.cleaned_data['multi_choice'], models.query.QuerySet) class ModelFormCallableModelDefault(TestCase): @@ -221,3 +229,31 @@ class RelatedModelFormTests(TestCase): model=A self.assertTrue(issubclass(ModelFormMetaclass(str('Form'), (ModelForm,), {'Meta': Meta}), ModelForm)) + + +class ManyToManyExclusionTestCase(TestCase): + def test_m2m_field_exclusion(self): + # Issue 12337. save_instance should honor the passed-in exclude keyword. + opt1 = ChoiceOptionModel.objects.create(id=1, name='default') + opt2 = ChoiceOptionModel.objects.create(id=2, name='option 2') + opt3 = ChoiceOptionModel.objects.create(id=3, name='option 3') + initial = { + 'choice': opt1, + 'choice_int': opt1, + } + data = { + 'choice': opt2.pk, + 'choice_int': opt2.pk, + 'multi_choice': 'string data!', + 'multi_choice_int': [opt1.pk], + } + instance = ChoiceFieldModel.objects.create(**initial) + instance.multi_choice = instance.multi_choice_int = [opt2, opt3] + form = ChoiceFieldExclusionForm(data=data, instance=instance) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['multi_choice'], data['multi_choice']) + form.save() + self.assertEqual(form.instance.choice.pk, data['choice']) + self.assertEqual(form.instance.choice_int.pk, data['choice_int']) + self.assertEqual(list(form.instance.multi_choice.all()), [opt2, opt3]) + self.assertEqual([obj.pk for obj in form.instance.multi_choice_int.all()], data['multi_choice_int']) diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py index 2acb982b9a..211df8aa3d 100644 --- a/tests/generic_relations/models.py +++ b/tests/generic_relations/models.py @@ -102,3 +102,22 @@ class Rock(Mineral): class ManualPK(models.Model): id = models.IntegerField(primary_key=True) tags = generic.GenericRelation(TaggedItem) + + +class ForProxyModelModel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + obj = generic.GenericForeignKey(for_concrete_model=False) + title = models.CharField(max_length=255, null=True) + +class ForConcreteModelModel(models.Model): + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + obj = generic.GenericForeignKey() + +class ConcreteRelatedModel(models.Model): + bases = generic.GenericRelation(ForProxyModelModel, for_concrete_model=False) + +class ProxyRelatedModel(ConcreteRelatedModel): + class Meta: + proxy = True diff --git a/tests/generic_relations/tests.py b/tests/generic_relations/tests.py index 79c7bc6184..734b2e5143 100644 --- a/tests/generic_relations/tests.py +++ b/tests/generic_relations/tests.py @@ -6,7 +6,9 @@ from django.contrib.contenttypes.models import ContentType from django.test import TestCase from .models import (TaggedItem, ValuableTaggedItem, Comparison, Animal, - Vegetable, Mineral, Gecko, Rock, ManualPK) + Vegetable, Mineral, Gecko, Rock, ManualPK, + ForProxyModelModel, ForConcreteModelModel, + ProxyRelatedModel, ConcreteRelatedModel) class GenericRelationsTests(TestCase): @@ -245,6 +247,22 @@ class GenericRelationsTests(TestCase): TaggedItem.objects.create(content_object=granite, tag="countertop") self.assertEqual(Rock.objects.filter(tags__tag="countertop").count(), 1) + def test_generic_inline_formsets_initial(self): + """ + Test for #17927 Initial values support for BaseGenericInlineFormSet. + """ + quartz = Mineral.objects.create(name="Quartz", hardness=7) + + GenericFormSet = generic_inlineformset_factory(TaggedItem, extra=1) + ctype = ContentType.objects.get_for_model(quartz) + initial_data = [{ + 'tag': 'lizard', + 'content_type': ctype.pk, + 'object_id': quartz.pk, + }] + formset = GenericFormSet(initial=initial_data) + self.assertEqual(formset.forms[0].initial, initial_data[0]) + class CustomWidget(forms.TextInput): pass @@ -256,12 +274,120 @@ class TaggedItemForm(forms.ModelForm): widgets = {'tag': CustomWidget} class GenericInlineFormsetTest(TestCase): - """ - Regression for #14572: Using base forms with widgets - defined in Meta should not raise errors. - """ - def test_generic_inlineformset_factory(self): + """ + Regression for #14572: Using base forms with widgets + defined in Meta should not raise errors. + """ Formset = generic_inlineformset_factory(TaggedItem, TaggedItemForm) form = Formset().forms[0] - self.assertTrue(isinstance(form['tag'].field.widget, CustomWidget)) + self.assertIsInstance(form['tag'].field.widget, CustomWidget) + + def test_save_new_for_proxy(self): + Formset = generic_inlineformset_factory(ForProxyModelModel, + fields='__all__', for_concrete_model=False) + + instance = ProxyRelatedModel.objects.create() + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-title': 'foo', + } + + formset = Formset(data, instance=instance, prefix='form') + self.assertTrue(formset.is_valid()) + + new_obj, = formset.save() + self.assertEqual(new_obj.obj, instance) + + def test_save_new_for_concrete(self): + Formset = generic_inlineformset_factory(ForProxyModelModel, + fields='__all__', for_concrete_model=True) + + instance = ProxyRelatedModel.objects.create() + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-title': 'foo', + } + + formset = Formset(data, instance=instance, prefix='form') + self.assertTrue(formset.is_valid()) + + new_obj, = formset.save() + self.assertNotIsInstance(new_obj.obj, ProxyRelatedModel) + + +class ProxyRelatedModelTest(TestCase): + def test_default_behavior(self): + """ + The default for for_concrete_model should be True + """ + base = ForConcreteModelModel() + base.obj = rel = ProxyRelatedModel.objects.create() + base.save() + + base = ForConcreteModelModel.objects.get(pk=base.pk) + rel = ConcreteRelatedModel.objects.get(pk=rel.pk) + self.assertEqual(base.obj, rel) + + def test_works_normally(self): + """ + When for_concrete_model is False, we should still be able to get + an instance of the concrete class. + """ + base = ForProxyModelModel() + base.obj = rel = ConcreteRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + self.assertEqual(base.obj, rel) + + def test_proxy_is_returned(self): + """ + Instances of the proxy should be returned when + for_concrete_model is False. + """ + base = ForProxyModelModel() + base.obj = ProxyRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + self.assertIsInstance(base.obj, ProxyRelatedModel) + + def test_query(self): + base = ForProxyModelModel() + base.obj = rel = ConcreteRelatedModel.objects.create() + base.save() + + self.assertEqual(rel, ConcreteRelatedModel.objects.get(bases__id=base.id)) + + def test_query_proxy(self): + base = ForProxyModelModel() + base.obj = rel = ProxyRelatedModel.objects.create() + base.save() + + self.assertEqual(rel, ProxyRelatedModel.objects.get(bases__id=base.id)) + + def test_generic_relation(self): + base = ForProxyModelModel() + base.obj = ProxyRelatedModel.objects.create() + base.save() + + base = ForProxyModelModel.objects.get(pk=base.pk) + rel = ProxyRelatedModel.objects.get(pk=base.obj.pk) + self.assertEqual(base, rel.bases.get()) + + def test_generic_relation_set(self): + base = ForProxyModelModel() + base.obj = ConcreteRelatedModel.objects.create() + base.save() + newrel = ConcreteRelatedModel.objects.create() + + newrel.bases = [base] + newrel = ConcreteRelatedModel.objects.get(pk=newrel.pk) + self.assertEqual(base, newrel.bases.get()) diff --git a/tests/generic_relations_regress/models.py b/tests/generic_relations_regress/models.py index 2795471f7f..d716f09058 100644 --- a/tests/generic_relations_regress/models.py +++ b/tests/generic_relations_regress/models.py @@ -122,3 +122,36 @@ class Tag(models.Model): class Board(models.Model): name = models.CharField(primary_key=True, max_length=15) + +class HasLinks(models.Model): + links = generic.GenericRelation(Link) + + class Meta: + abstract = True + +class HasLinkThing(HasLinks): + pass + +class A(models.Model): + flag = models.NullBooleanField() + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + +class B(models.Model): + a = generic.GenericRelation(A) + + class Meta: + ordering = ('id',) + +class C(models.Model): + b = models.ForeignKey(B) + + class Meta: + ordering = ('id',) + +class D(models.Model): + b = models.ForeignKey(B, null=True) + + class Meta: + ordering = ('id',) diff --git a/tests/generic_relations_regress/tests.py b/tests/generic_relations_regress/tests.py index 474113584a..75edff34f2 100644 --- a/tests/generic_relations_regress/tests.py +++ b/tests/generic_relations_regress/tests.py @@ -2,9 +2,10 @@ from django.db.models import Q from django.db.utils import IntegrityError from django.test import TestCase, skipIfDBFeature -from .models import (Address, Place, Restaurant, Link, CharLink, TextLink, +from .models import ( + Address, Place, Restaurant, Link, CharLink, TextLink, Person, Contact, Note, Organization, OddRelation1, OddRelation2, Company, - Developer, Team, Guild, Tag, Board) + Developer, Team, Guild, Tag, Board, HasLinkThing, A, B, C, D) class GenericRelationTests(TestCase): @@ -28,9 +29,9 @@ class GenericRelationTests(TestCase): originating model of a query. See #12664. """ p = Person.objects.create(account=23, name='Chef') - a = Address.objects.create(street='123 Anywhere Place', - city='Conifer', state='CO', - zipcode='80433', content_object=p) + Address.objects.create(street='123 Anywhere Place', + city='Conifer', state='CO', + zipcode='80433', content_object=p) qs = Person.objects.filter(addresses__zipcode='80433') self.assertEqual(1, qs.count()) @@ -38,12 +39,12 @@ class GenericRelationTests(TestCase): def test_charlink_delete(self): oddrel = OddRelation1.objects.create(name='clink') - cl = CharLink.objects.create(content_object=oddrel) + CharLink.objects.create(content_object=oddrel) oddrel.delete() def test_textlink_delete(self): oddrel = OddRelation2.objects.create(name='tlink') - tl = TextLink.objects.create(content_object=oddrel) + TextLink.objects.create(content_object=oddrel) oddrel.delete() def test_q_object_or(self): @@ -61,12 +62,12 @@ class GenericRelationTests(TestCase): """ note_contact = Contact.objects.create() org_contact = Contact.objects.create() - note = Note.objects.create(note='note', content_object=note_contact) + Note.objects.create(note='note', content_object=note_contact) org = Organization.objects.create(name='org name') org.contacts.add(org_contact) # search with a non-matching note and a matching org name qs = Contact.objects.filter(Q(notes__note__icontains=r'other note') | - Q(organizations__name__icontains=r'org name')) + Q(organizations__name__icontains=r'org name')) self.assertTrue(org_contact in qs) # search again, with the same query parameters, in reverse order qs = Contact.objects.filter( @@ -90,8 +91,8 @@ class GenericRelationTests(TestCase): p1 = Place.objects.create(name="South Park") p2 = Place.objects.create(name="The City") c = Company.objects.create(name="Chubby's Intl.") - l1 = Link.objects.create(content_object=p1) - l2 = Link.objects.create(content_object=c) + Link.objects.create(content_object=p1) + Link.objects.create(content_object=c) places = list(Place.objects.order_by('links__id')) def count_places(place): @@ -114,8 +115,10 @@ class GenericRelationTests(TestCase): try: note = Note(note='Deserve a bonus', content_object=team1) except Exception as e: - if issubclass(type(e), Exception) and str(e) == 'Impossible arguments to GFK.get_content_type!': - self.fail("Saving model with GenericForeignKey to model instance whose __len__ method returns 0 shouldn't fail.") + if (issubclass(type(e), Exception) and + str(e) == 'Impossible arguments to GFK.get_content_type!'): + self.fail("Saving model with GenericForeignKey to model instance whose " + "__len__ method returns 0 shouldn't fail.") raise e note.save() @@ -135,3 +138,77 @@ class GenericRelationTests(TestCase): b1 = Board.objects.create(name='') tag = Tag(label='VP', content_object=b1) tag.save() + + def test_ticket_20378(self): + hs1 = HasLinkThing.objects.create() + hs2 = HasLinkThing.objects.create() + l1 = Link.objects.create(content_object=hs1) + l2 = Link.objects.create(content_object=hs2) + self.assertQuerysetEqual( + HasLinkThing.objects.filter(links=l1), + [hs1], lambda x: x) + self.assertQuerysetEqual( + HasLinkThing.objects.filter(links=l2), + [hs2], lambda x: x) + self.assertQuerysetEqual( + HasLinkThing.objects.exclude(links=l2), + [hs1], lambda x: x) + self.assertQuerysetEqual( + HasLinkThing.objects.exclude(links=l1), + [hs2], lambda x: x) + + def test_ticket_20564(self): + b1 = B.objects.create() + b2 = B.objects.create() + b3 = B.objects.create() + c1 = C.objects.create(b=b1) + c2 = C.objects.create(b=b2) + c3 = C.objects.create(b=b3) + A.objects.create(flag=None, content_object=b1) + A.objects.create(flag=True, content_object=b2) + self.assertQuerysetEqual( + C.objects.filter(b__a__flag=None), + [c1, c3], lambda x: x + ) + self.assertQuerysetEqual( + C.objects.exclude(b__a__flag=None), + [c2], lambda x: x + ) + + def test_ticket_20564_nullable_fk(self): + b1 = B.objects.create() + b2 = B.objects.create() + b3 = B.objects.create() + d1 = D.objects.create(b=b1) + d2 = D.objects.create(b=b2) + d3 = D.objects.create(b=b3) + d4 = D.objects.create() + A.objects.create(flag=None, content_object=b1) + A.objects.create(flag=True, content_object=b1) + A.objects.create(flag=True, content_object=b2) + self.assertQuerysetEqual( + D.objects.exclude(b__a__flag=None), + [d2], lambda x: x + ) + self.assertQuerysetEqual( + D.objects.filter(b__a__flag=None), + [d1, d3, d4], lambda x: x + ) + self.assertQuerysetEqual( + B.objects.filter(a__flag=None), + [b1, b3], lambda x: x + ) + self.assertQuerysetEqual( + B.objects.exclude(a__flag=None), + [b2], lambda x: x + ) + + def test_extra_join_condition(self): + # A crude check that content_type_id is taken in account in the + # join/subquery condition. + self.assertIn("content_type_id", str(B.objects.exclude(a__flag=None).query).lower()) + # No need for any joins - the join from inner query can be trimmed in + # this case (but not in the above case as no a objects at all for given + # B would then fail). + self.assertNotIn(" join ", str(B.objects.exclude(a__flag=True).query).lower()) + self.assertIn("content_type_id", str(B.objects.exclude(a__flag=True).query).lower()) diff --git a/tests/generic_views/test_base.py b/tests/generic_views/test_base.py index 0e84e17132..ffd9b1b480 100644 --- a/tests/generic_views/test_base.py +++ b/tests/generic_views/test_base.py @@ -278,7 +278,7 @@ class TemplateViewTest(TestCase): response = self.client.get('/template/simple/bar/') self.assertEqual(response.status_code, 200) self.assertEqual(response.context['foo'], 'bar') - self.assertTrue(isinstance(response.context['view'], View)) + self.assertIsInstance(response.context['view'], View) def test_extra_template_params(self): """ @@ -288,7 +288,7 @@ class TemplateViewTest(TestCase): self.assertEqual(response.status_code, 200) self.assertEqual(response.context['foo'], 'bar') self.assertEqual(response.context['key'], 'value') - self.assertTrue(isinstance(response.context['view'], View)) + self.assertIsInstance(response.context['view'], View) def test_cached_views(self): """ @@ -384,6 +384,12 @@ class RedirectViewTest(unittest.TestCase): self.assertEqual(response.status_code, 301) self.assertEqual(response.url, '/bar/') + def test_redirect_PATCH(self): + "Default is a permanent redirect" + response = RedirectView.as_view(url='/bar/')(self.rf.patch('/foo/')) + self.assertEqual(response.status_code, 301) + self.assertEqual(response.url, '/bar/') + def test_redirect_DELETE(self): "Default is a permanent redirect" response = RedirectView.as_view(url='/bar/')(self.rf.delete('/foo/')) @@ -411,3 +417,36 @@ class GetContextDataTest(unittest.TestCase): # test that kwarg overrides values assigned higher up context = test_view.get_context_data(test_name='test_value') self.assertEqual(context['test_name'], 'test_value') + + def test_object_at_custom_name_in_context_data(self): + # Checks 'pony' key presence in dict returned by get_context_date + test_view = views.CustomSingleObjectView() + test_view.context_object_name = 'pony' + context = test_view.get_context_data() + self.assertEqual(context['pony'], test_view.object) + + def test_object_in_get_context_data(self): + # Checks 'object' key presence in dict returned by get_context_date #20234 + test_view = views.CustomSingleObjectView() + context = test_view.get_context_data() + self.assertEqual(context['object'], test_view.object) + + +class UseMultipleObjectMixinTest(unittest.TestCase): + rf = RequestFactory() + + def test_use_queryset_from_view(self): + test_view = views.CustomMultipleObjectMixinView() + test_view.get(self.rf.get('/')) + # Don't pass queryset as argument + context = test_view.get_context_data() + self.assertEqual(context['object_list'], test_view.queryset) + + def test_overwrite_queryset(self): + test_view = views.CustomMultipleObjectMixinView() + test_view.get(self.rf.get('/')) + queryset = [{'name': 'Lennon'}, {'name': 'Ono'}] + self.assertNotEqual(test_view.queryset, queryset) + # Overwrite the view's queryset with queryset from kwarg + context = test_view.get_context_data(object_list=queryset) + self.assertEqual(context['object_list'], queryset) diff --git a/tests/generic_views/test_detail.py b/tests/generic_views/test_detail.py index faee05688e..3a97d27995 100644 --- a/tests/generic_views/test_detail.py +++ b/tests/generic_views/test_detail.py @@ -15,7 +15,7 @@ class DetailViewTest(TestCase): res = self.client.get('/detail/obj/') self.assertEqual(res.status_code, 200) self.assertEqual(res.context['object'], {'foo': 'bar'}) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['view'], View) self.assertTemplateUsed(res, 'generic_views/detail.html') def test_detail_by_pk(self): diff --git a/tests/generic_views/test_edit.py b/tests/generic_views/test_edit.py index 54eab7ffa4..435e48ba99 100644 --- a/tests/generic_views/test_edit.py +++ b/tests/generic_views/test_edit.py @@ -43,8 +43,8 @@ class CreateViewTests(TestCase): def test_create(self): res = self.client.get('/edit/authors/create/') self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['form'], forms.ModelForm) + self.assertIsInstance(res.context['view'], View) self.assertFalse('object' in res.context) self.assertFalse('author' in res.context) self.assertTemplateUsed(res, 'generic_views/author_form.html') @@ -89,7 +89,7 @@ class CreateViewTests(TestCase): def test_create_with_special_properties(self): res = self.client.get('/edit/authors/create/special/') self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], views.AuthorForm)) + self.assertIsInstance(res.context['form'], views.AuthorForm) self.assertFalse('object' in res.context) self.assertFalse('author' in res.context) self.assertTemplateUsed(res, 'generic_views/form.html') @@ -165,7 +165,7 @@ class UpdateViewTests(TestCase): ) res = self.client.get('/edit/author/%d/update/' % a.pk) self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) + self.assertIsInstance(res.context['form'], forms.ModelForm) self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk)) self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk)) self.assertTemplateUsed(res, 'generic_views/author_form.html') @@ -247,7 +247,7 @@ class UpdateViewTests(TestCase): ) res = self.client.get('/edit/author/%d/update/special/' % a.pk) self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], views.AuthorForm)) + self.assertIsInstance(res.context['form'], views.AuthorForm) self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk)) self.assertEqual(res.context['thingy'], Author.objects.get(pk=a.pk)) self.assertFalse('author' in res.context) @@ -279,8 +279,8 @@ class UpdateViewTests(TestCase): ) res = self.client.get('/edit/author/update/') self.assertEqual(res.status_code, 200) - self.assertTrue(isinstance(res.context['form'], forms.ModelForm)) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['form'], forms.ModelForm) + self.assertIsInstance(res.context['view'], View) self.assertEqual(res.context['object'], Author.objects.get(pk=a.pk)) self.assertEqual(res.context['author'], Author.objects.get(pk=a.pk)) self.assertTemplateUsed(res, 'generic_views/author_form.html') diff --git a/tests/generic_views/test_list.py b/tests/generic_views/test_list.py index cc4d2f5966..a77a6418a3 100644 --- a/tests/generic_views/test_list.py +++ b/tests/generic_views/test_list.py @@ -24,7 +24,7 @@ class ListViewTests(TestCase): self.assertEqual(res.status_code, 200) self.assertTemplateUsed(res, 'generic_views/author_list.html') self.assertEqual(list(res.context['object_list']), list(Author.objects.all())) - self.assertTrue(isinstance(res.context['view'], View)) + self.assertIsInstance(res.context['view'], View) self.assertIs(res.context['author_list'], res.context['object_list']) self.assertIsNone(res.context['paginator']) self.assertIsNone(res.context['page_obj']) diff --git a/tests/generic_views/views.py b/tests/generic_views/views.py index aa8777e8c6..fd331f14b7 100644 --- a/tests/generic_views/views.py +++ b/tests/generic_views/views.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from django.contrib.auth.decorators import login_required -from django.contrib.messages.views import SuccessMessageMixin from django.core.paginator import Paginator from django.core.urlresolvers import reverse, reverse_lazy from django.utils.decorators import method_decorator @@ -201,6 +200,17 @@ class BookDetailGetObjectCustomQueryset(BookDetail): return super(BookDetailGetObjectCustomQueryset,self).get_object( queryset=Book.objects.filter(pk=2)) + +class CustomMultipleObjectMixinView(generic.list.MultipleObjectMixin, generic.View): + queryset = [ + {'name': 'John'}, + {'name': 'Yoko'}, + ] + + def get(self, request): + self.object_list = self.get_queryset() + + class CustomContextView(generic.detail.SingleObjectMixin, generic.View): model = Book object = Book(name='dummy') @@ -216,6 +226,10 @@ class CustomContextView(generic.detail.SingleObjectMixin, generic.View): def get_context_object_name(self, obj): return "test_name" +class CustomSingleObjectView(generic.detail.SingleObjectMixin, generic.View): + model = Book + object = Book(name="dummy") + class BookSigningConfig(object): model = BookSigning date_field = 'event_date' diff --git a/tests/get_earliest_or_latest/tests.py b/tests/get_earliest_or_latest/tests.py index 6317a0974c..8d16af9587 100644 --- a/tests/get_earliest_or_latest/tests.py +++ b/tests/get_earliest_or_latest/tests.py @@ -121,3 +121,34 @@ class EarliestOrLatestTests(TestCase): p2 = Person.objects.create(name="Stephanie", birthday=datetime(1960, 2, 3)) self.assertRaises(AssertionError, Person.objects.latest) self.assertEqual(Person.objects.latest("birthday"), p2) + + def test_first(self): + p1 = Person.objects.create(name="Bob", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create(name="Alice", birthday=datetime(1961, 2, 3)) + self.assertEqual( + Person.objects.first(), p1) + self.assertEqual( + Person.objects.order_by('name').first(), p2) + self.assertEqual( + Person.objects.filter(birthday__lte=datetime(1955, 1, 1)).first(), + p1) + self.assertIs( + Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).first(), + None) + + def test_last(self): + p1 = Person.objects.create( + name="Alice", birthday=datetime(1950, 1, 1)) + p2 = Person.objects.create( + name="Bob", birthday=datetime(1960, 2, 3)) + # Note: by default PK ordering. + self.assertEqual( + Person.objects.last(), p2) + self.assertEqual( + Person.objects.order_by('-name').last(), p1) + self.assertEqual( + Person.objects.filter(birthday__lte=datetime(1955, 1, 1)).last(), + p1) + self.assertIs( + Person.objects.filter(birthday__lte=datetime(1940, 1, 1)).last(), + None) diff --git a/tests/get_or_create/models.py b/tests/get_or_create/models.py index 82905de4f8..1a85de2e74 100644 --- a/tests/get_or_create/models.py +++ b/tests/get_or_create/models.py @@ -21,6 +21,11 @@ class Person(models.Model): def __str__(self): return '%s %s' % (self.first_name, self.last_name) + +class DefaultPerson(models.Model): + first_name = models.CharField(max_length=100, default="Anonymous") + + class ManualPrimaryKeyTest(models.Model): id = models.IntegerField(primary_key=True) data = models.CharField(max_length=100) @@ -28,3 +33,12 @@ class ManualPrimaryKeyTest(models.Model): class Profile(models.Model): person = models.ForeignKey(Person, primary_key=True) + + +class Tag(models.Model): + text = models.CharField(max_length=255, unique=True) + + +class Thing(models.Model): + name = models.CharField(max_length=256) + tags = models.ManyToManyField(Tag) diff --git a/tests/get_or_create/tests.py b/tests/get_or_create/tests.py index e9cce9bbde..36c248b169 100644 --- a/tests/get_or_create/tests.py +++ b/tests/get_or_create/tests.py @@ -2,14 +2,17 @@ from __future__ import absolute_import from datetime import date import traceback +import warnings -from django.db import IntegrityError +from django.db import IntegrityError, DatabaseError +from django.utils.encoding import DjangoUnicodeDecodeError from django.test import TestCase, TransactionTestCase -from .models import Person, ManualPrimaryKeyTest, Profile +from .models import DefaultPerson, Person, ManualPrimaryKeyTest, Profile, Tag, Thing class GetOrCreateTests(TestCase): + def test_get_or_create(self): p = Person.objects.create( first_name='John', last_name='Lennon', birthday=date(1940, 10, 9) @@ -64,6 +67,30 @@ class GetOrCreateTests(TestCase): formatted_traceback = traceback.format_exc() self.assertIn('obj.save', formatted_traceback) + def test_savepoint_rollback(self): + # Regression test for #20463: the database connection should still be + # usable after a DataError or ProgrammingError in .get_or_create(). + try: + # Hide warnings when broken data is saved with a warning (MySQL). + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + Person.objects.get_or_create( + birthday=date(1970, 1, 1), + defaults={'first_name': b"\xff", 'last_name': b"\xff"}) + except (DatabaseError, DjangoUnicodeDecodeError): + Person.objects.create( + first_name="Bob", last_name="Ross", birthday=date(1950, 1, 1)) + else: + self.skipTest("This backend accepts broken utf-8.") + + def test_get_or_create_empty(self): + # Regression test for #16137: get_or_create does not require kwargs. + try: + DefaultPerson.objects.get_or_create() + except AssertionError: + self.fail("If all the attributes on a model have defaults, we " + "shouldn't need to pass any arguments.") + class GetOrCreateTransactionTests(TransactionTestCase): @@ -77,3 +104,28 @@ class GetOrCreateTransactionTests(TransactionTestCase): pass else: self.skipTest("This backend does not support integrity checks.") + + +class GetOrCreateThroughManyToMany(TestCase): + + def test_get_get_or_create(self): + tag = Tag.objects.create(text='foo') + a_thing = Thing.objects.create(name='a') + a_thing.tags.add(tag) + obj, created = a_thing.tags.get_or_create(text='foo') + + self.assertFalse(created) + self.assertEqual(obj.pk, tag.pk) + + def test_create_get_or_create(self): + a_thing = Thing.objects.create(name='a') + obj, created = a_thing.tags.get_or_create(text='foo') + + self.assertTrue(created) + self.assertEqual(obj.text, 'foo') + self.assertIn(obj, a_thing.tags.all()) + + def test_something(self): + Tag.objects.create(text='foo') + a_thing = Thing.objects.create(name='a') + self.assertRaises(IntegrityError, a_thing.tags.get_or_create, text='foo') diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 3680eecdd2..397b63647a 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -61,6 +61,7 @@ class TransactionsPerRequestTests(TransactionTestCase): connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests self.assertContains(response, 'False') + class SignalsTests(TestCase): urls = 'handlers.urls' @@ -89,3 +90,11 @@ class SignalsTests(TestCase): self.assertEqual(self.signals, ['started']) self.assertEqual(b''.join(response.streaming_content), b"streaming content") self.assertEqual(self.signals, ['started', 'finished']) + + +class HandlerSuspiciousOpsTest(TestCase): + urls = 'handlers.urls' + + def test_suspiciousop_in_view_returns_400(self): + response = self.client.get('/suspicious/') + self.assertEqual(response.status_code, 400) diff --git a/tests/handlers/urls.py b/tests/handlers/urls.py index 29858055ab..eaf764b00b 100644 --- a/tests/handlers/urls.py +++ b/tests/handlers/urls.py @@ -9,4 +9,5 @@ urlpatterns = patterns('', url(r'^streaming/$', views.streaming), url(r'^in_transaction/$', views.in_transaction), url(r'^not_in_transaction/$', views.not_in_transaction), + url(r'^suspicious/$', views.suspicious), ) diff --git a/tests/handlers/views.py b/tests/handlers/views.py index 22d9ea4c7d..1b75b27043 100644 --- a/tests/handlers/views.py +++ b/tests/handlers/views.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals -from django.db import connection +from django.core.exceptions import SuspiciousOperation +from django.db import connection, transaction from django.http import HttpResponse, StreamingHttpResponse def regular(request): @@ -12,6 +13,9 @@ def streaming(request): def in_transaction(request): return HttpResponse(str(connection.in_atomic_block)) +@transaction.non_atomic_requests def not_in_transaction(request): return HttpResponse(str(connection.in_atomic_block)) -not_in_transaction.transactions_per_request = False + +def suspicious(request): + raise SuspiciousOperation('dubious') diff --git a/tests/http_utils/tests.py b/tests/http_utils/tests.py index 7dfd24d721..9f99c94da7 100644 --- a/tests/http_utils/tests.py +++ b/tests/http_utils/tests.py @@ -1,10 +1,24 @@ from __future__ import unicode_literals +import io +import gzip + from django.http import HttpRequest, HttpResponse, StreamingHttpResponse from django.http.utils import conditional_content_removal from django.test import TestCase +# based on Python 3.3's gzip.compress +def gzip_compress(data): + buf = io.BytesIO() + f = gzip.GzipFile(fileobj=buf, mode='wb', compresslevel=0) + try: + f.write(data) + finally: + f.close() + return buf.getvalue() + + class HttpUtilTests(TestCase): def test_conditional_content_removal(self): @@ -33,6 +47,19 @@ class HttpUtilTests(TestCase): conditional_content_removal(req, res) self.assertEqual(b''.join(res), b'') + # Issue #20472 + abc = gzip_compress(b'abc') + res = HttpResponse(abc, status=304) + res['Content-Encoding'] = 'gzip' + conditional_content_removal(req, res) + self.assertEqual(res.content, b'') + + res = StreamingHttpResponse([abc], status=304) + res['Content-Encoding'] = 'gzip' + conditional_content_removal(req, res) + self.assertEqual(b''.join(res), b'') + + # Strip content for HEAD requests. req.method = 'HEAD' diff --git a/tests/i18n/__init__.py b/tests/i18n/__init__.py index e69de29bb2..c5aaa31fe3 100644 --- a/tests/i18n/__init__.py +++ b/tests/i18n/__init__.py @@ -0,0 +1,18 @@ +from threading import local + + +class TransRealMixin(object): + """This is the only way to reset the translation machinery. Otherwise + the test suite occasionally fails because of global state pollution + between tests.""" + def flush_caches(self): + from django.utils.translation import trans_real + trans_real._translations = {} + trans_real._active = local() + trans_real._default = None + trans_real._accepted = {} + trans_real._checked_languages = {} + + def tearDown(self): + self.flush_caches() + super(TransRealMixin, self).tearDown() diff --git a/tests/i18n/commands/extraction.py b/tests/i18n/commands/extraction.py index 7c482e58fb..8696ae453b 100644 --- a/tests/i18n/commands/extraction.py +++ b/tests/i18n/commands/extraction.py @@ -279,17 +279,22 @@ class IgnoredExtractorTests(ExtractorTests): def test_ignore_option(self): os.chdir(self.test_dir) - pattern1 = os.path.join('ignore_dir', '*') + ignore_patterns = [ + os.path.join('ignore_dir', '*'), + 'xxx_*', + ] stdout = StringIO() management.call_command('makemessages', locale=LOCALE, verbosity=2, - ignore_patterns=[pattern1], stdout=stdout) + ignore_patterns=ignore_patterns, stdout=stdout) data = stdout.getvalue() self.assertTrue("ignoring directory ignore_dir" in data) + self.assertTrue("ignoring file xxx_ignored.html" in data) self.assertTrue(os.path.exists(self.PO_FILE)) with open(self.PO_FILE, 'r') as fp: po_contents = fp.read() self.assertMsgId('This literal should be included.', po_contents) self.assertNotMsgId('This should be ignored.', po_contents) + self.assertNotMsgId('This should be ignored too.', po_contents) class SymlinkExtractorTests(ExtractorTests): diff --git a/tests/i18n/commands/templates/xxx_ignored.html b/tests/i18n/commands/templates/xxx_ignored.html new file mode 100644 index 0000000000..a41cbe202a --- /dev/null +++ b/tests/i18n/commands/templates/xxx_ignored.html @@ -0,0 +1,2 @@ +{% load i18n %} +{% trans "This should be ignored too." %} diff --git a/tests/i18n/commands/tests.py b/tests/i18n/commands/tests.py deleted file mode 100644 index f9e3c20fff..0000000000 --- a/tests/i18n/commands/tests.py +++ /dev/null @@ -1,24 +0,0 @@ -import os -import re -from subprocess import Popen, PIPE - -from django.core.management.utils import find_command - -can_run_extraction_tests = False -can_run_compilation_tests = False - -# checks if it can find xgettext on the PATH and -# imports the extraction tests if yes -xgettext_cmd = find_command('xgettext') -if xgettext_cmd: - p = Popen('%s --version' % xgettext_cmd, shell=True, stdout=PIPE, stderr=PIPE, close_fds=os.name != 'nt', universal_newlines=True) - output = p.communicate()[0] - match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', output) - if match: - xversion = (int(match.group('major')), int(match.group('minor'))) - if xversion >= (0, 15): - can_run_extraction_tests = True - del p - -if find_command('msgfmt'): - can_run_compilation_tests = True diff --git a/tests/i18n/contenttypes/tests.py b/tests/i18n/contenttypes/tests.py index 5e8a9823e1..cbac9ec5da 100644 --- a/tests/i18n/contenttypes/tests.py +++ b/tests/i18n/contenttypes/tests.py @@ -10,6 +10,8 @@ from django.utils._os import upath from django.utils import six from django.utils import translation +from i18n import TransRealMixin + @override_settings( USE_I18N=True, @@ -22,7 +24,7 @@ from django.utils import translation ('fr', 'French'), ), ) -class ContentTypeTests(TestCase): +class ContentTypeTests(TransRealMixin, TestCase): def test_verbose_name(self): company_type = ContentType.objects.get(app_label='i18n', model='company') with translation.override('en'): diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py index d334e4fe2d..85eb7db084 100644 --- a/tests/i18n/patterns/tests.py +++ b/tests/i18n/patterns/tests.py @@ -52,8 +52,10 @@ class URLPrefixTests(URLTestCaseBase): def test_not_prefixed(self): with translation.override('en'): self.assertEqual(reverse('not-prefixed'), '/not-prefixed/') + self.assertEqual(reverse('not-prefixed-included-url'), '/not-prefixed-include/foo/') with translation.override('nl'): self.assertEqual(reverse('not-prefixed'), '/not-prefixed/') + self.assertEqual(reverse('not-prefixed-included-url'), '/not-prefixed-include/foo/') def test_prefixed(self): with translation.override('en'): @@ -183,7 +185,7 @@ class URLRedirectTests(URLTestCaseBase): class URLVaryAcceptLanguageTests(URLTestCaseBase): """ Tests that 'Accept-Language' is not added to the Vary header when using - prefixed URLs. + prefixed URLs. """ def test_no_prefix_response(self): response = self.client.get('/not-prefixed/') diff --git a/tests/i18n/patterns/urls/default.py b/tests/i18n/patterns/urls/default.py index 0edaea48ad..ff52e26227 100644 --- a/tests/i18n/patterns/urls/default.py +++ b/tests/i18n/patterns/urls/default.py @@ -8,6 +8,7 @@ view = TemplateView.as_view(template_name='dummy.html') urlpatterns = patterns('', url(r'^not-prefixed/$', view, name='not-prefixed'), + url(r'^not-prefixed-include/', include('i18n.patterns.urls.included')), url(_(r'^translated/$'), view, name='no-prefix-translated'), url(_(r'^translated/(?P<slug>[\w-]+)/$'), view, name='no-prefix-translated-slug'), ) diff --git a/tests/i18n/patterns/urls/included.py b/tests/i18n/patterns/urls/included.py new file mode 100644 index 0000000000..3f3d1325dd --- /dev/null +++ b/tests/i18n/patterns/urls/included.py @@ -0,0 +1,9 @@ +from django.conf.urls import patterns, url +from django.views.generic import TemplateView + + +view = TemplateView.as_view(template_name='dummy.html') + +urlpatterns = patterns('', + url(r'^foo/$', view, name='not-prefixed-included-url'), +) diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index 1022c8d2f1..019ed88cdb 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -8,6 +8,7 @@ import pickle from threading import local from django.conf import settings +from django.core.management.utils import find_command from django.template import Template, Context from django.template.base import TemplateSyntaxError from django.test import TestCase, RequestFactory @@ -30,19 +31,20 @@ from django.utils.translation import (activate, deactivate, ngettext, ngettext_lazy, ungettext, ungettext_lazy, pgettext, pgettext_lazy, - npgettext, npgettext_lazy) + npgettext, npgettext_lazy, + check_for_language) -from .commands.tests import can_run_extraction_tests, can_run_compilation_tests -if can_run_extraction_tests: +if find_command('xgettext'): from .commands.extraction import (ExtractorTests, BasicExtractorTests, JavascriptExtractorTests, IgnoredExtractorTests, SymlinkExtractorTests, CopyPluralFormsExtractorTests, NoWrapExtractorTests, NoLocationExtractorTests, KeepPotFileExtractorTests, MultipleLocaleExtractionTests) -if can_run_compilation_tests: +if find_command('msgfmt'): from .commands.compilation import (PoFileTests, PoFileContentsTests, PercentRenderingTests, MultipleLocaleCompilationTests, CompilationErrorHandling) +from . import TransRealMixin from .forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from .models import Company, TestModel @@ -52,7 +54,8 @@ extended_locale_paths = settings.LOCALE_PATHS + ( os.path.join(here, 'other', 'locale'), ) -class TranslationTests(TestCase): + +class TranslationTests(TransRealMixin, TestCase): def test_override(self): activate('de') @@ -333,10 +336,43 @@ class TranslationTests(TestCase): self.assertEqual(rendered, 'My other name is James.') -@override_settings(USE_L10N=True) -class FormattingTests(TestCase): +class TranslationThreadSafetyTests(TestCase): + """Specifically not using TransRealMixin here to test threading.""" def setUp(self): + self._old_language = get_language() + self._translations = trans_real._translations + + # here we rely on .split() being called inside the _fetch() + # in trans_real.translation() + class sideeffect_str(str): + def split(self, *args, **kwargs): + res = str.split(self, *args, **kwargs) + trans_real._translations['en-YY'] = None + return res + + trans_real._translations = {sideeffect_str('en-XX'): None} + + def tearDown(self): + trans_real._translations = self._translations + activate(self._old_language) + + def test_bug14894_translation_activate_thread_safety(self): + translation_count = len(trans_real._translations) + try: + translation.activate('pl') + except RuntimeError: + self.fail('translation.activate() is not thread-safe') + + # make sure sideeffect_str actually added a new translation + self.assertLess(translation_count, len(trans_real._translations)) + + +@override_settings(USE_L10N=True) +class FormattingTests(TransRealMixin, TestCase): + + def setUp(self): + super(FormattingTests, self).setUp() self.n = decimal.Decimal('66666.666') self.f = 99999.999 self.d = datetime.date(2009, 12, 31) @@ -738,9 +774,10 @@ class FormattingTests(TestCase): self.assertEqual(template2.render(context), output2) self.assertEqual(template3.render(context), output3) -class MiscTests(TestCase): +class MiscTests(TransRealMixin, TestCase): def setUp(self): + super(MiscTests, self).setUp() self.rf = RequestFactory() def test_parse_spec_http_header(self): @@ -884,17 +921,15 @@ class MiscTests(TestCase): self.assertEqual(t_plur.render(Context({'percent': 42, 'num': 4})), '%(percent)s% represents 4 objects') -class ResolutionOrderI18NTests(TestCase): +class ResolutionOrderI18NTests(TransRealMixin, TestCase): def setUp(self): - # Okay, this is brutal, but we have no other choice to fully reset - # the translation framework - trans_real._active = local() - trans_real._translations = {} + super(ResolutionOrderI18NTests, self).setUp() activate('de') def tearDown(self): deactivate() + super(ResolutionOrderI18NTests, self).tearDown() def assertUgettext(self, msgid, msgstr): result = ugettext(msgid) @@ -967,15 +1002,17 @@ class TestLanguageInfo(TestCase): six.assertRaisesRegex(self, KeyError, r"Unknown language code xx-xx and xx\.", get_language_info, 'xx-xx') -class MultipleLocaleActivationTests(TestCase): +class MultipleLocaleActivationTests(TransRealMixin, TestCase): """ Tests for template rendering behavior when multiple locales are activated during the lifetime of the same process. """ def setUp(self): + super(MultipleLocaleActivationTests, self).setUp() self._old_language = get_language() def tearDown(self): + super(MultipleLocaleActivationTests, self).tearDown() activate(self._old_language) def test_single_locale_activation(self): @@ -1104,7 +1141,7 @@ class MultipleLocaleActivationTests(TestCase): 'django.middleware.common.CommonMiddleware', ), ) -class LocaleMiddlewareTests(TestCase): +class LocaleMiddlewareTests(TransRealMixin, TestCase): urls = 'i18n.urls' @@ -1114,3 +1151,86 @@ class LocaleMiddlewareTests(TestCase): self.assertContains(response, "Oui/Non") response = self.client.get('/en/streaming/') self.assertContains(response, "Yes/No") + + @override_settings( + MIDDLEWARE_CLASSES=( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + ), + ) + def test_session_language(self): + """ + Check that language is stored in session if missing. + """ + # Create an empty session + engine = import_module(settings.SESSION_ENGINE) + session = engine.SessionStore() + session.save() + self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key + + # Clear the session data before request + session.save() + response = self.client.get('/en/simple/') + self.assertEqual(self.client.session['django_language'], 'en') + + # Clear the session data before request + session.save() + response = self.client.get('/fr/simple/') + self.assertEqual(self.client.session['django_language'], 'fr') + + # Check that language is not changed in session + response = self.client.get('/en/simple/') + self.assertEqual(self.client.session['django_language'], 'fr') + + +@override_settings( + USE_I18N=True, + LANGUAGES=( + ('bg', 'Bulgarian'), + ('en-us', 'English'), + ('pt-br', 'Portugese (Brazil)'), + ), + MIDDLEWARE_CLASSES=( + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + ), +) +class CountrySpecificLanguageTests(TransRealMixin, TestCase): + + urls = 'i18n.urls' + + def setUp(self): + super(CountrySpecificLanguageTests, self).setUp() + self.rf = RequestFactory() + + def test_check_for_language(self): + self.assertTrue(check_for_language('en')) + self.assertTrue(check_for_language('en-us')) + self.assertTrue(check_for_language('en-US')) + + def test_get_language_from_request(self): + # issue 19919 + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.8,bg;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('en-us', lang) + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'bg-bg,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('bg', lang) + + def test_specific_language_codes(self): + # issue 11915 + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('pt-br', lang) + r = self.rf.get('/') + r.COOKIES = {} + r.META = {'HTTP_ACCEPT_LANGUAGE': 'pt-pt,en-US;q=0.8,en;q=0.6,ru;q=0.4'} + lang = get_language_from_request(r) + self.assertEqual('pt-br', lang) diff --git a/tests/i18n/urls.py b/tests/i18n/urls.py index c118265dab..c7d01897f5 100644 --- a/tests/i18n/urls.py +++ b/tests/i18n/urls.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals from django.conf.urls.i18n import i18n_patterns -from django.http import StreamingHttpResponse +from django.http import HttpResponse, StreamingHttpResponse from django.utils.translation import ugettext_lazy as _ urlpatterns = i18n_patterns('', + (r'^simple/$', lambda r: HttpResponse()), (r'^streaming/$', lambda r: StreamingHttpResponse([_("Yes"), "/", _("No")])), ) diff --git a/tests/invalid_models/invalid_models/models.py b/tests/invalid_models/invalid_models/models.py index 3c21e1ddb8..e1bac9cfb2 100644 --- a/tests/invalid_models/invalid_models/models.py +++ b/tests/invalid_models/invalid_models/models.py @@ -25,6 +25,7 @@ class FieldErrors(models.Model): index = models.CharField(max_length=10, db_index='bad') field_ = models.CharField(max_length=10) nullbool = models.BooleanField(null=True) + generic_ip_notnull_blank = models.GenericIPAddressField(null=False, blank=True) class Target(models.Model): @@ -375,11 +376,12 @@ invalid_models.fielderrors: "decimalfield3": DecimalFields require a "max_digits invalid_models.fielderrors: "decimalfield4": DecimalFields require a "max_digits" attribute value that is greater than or equal to the value of the "decimal_places" attribute. invalid_models.fielderrors: "filefield": FileFields require an "upload_to" attribute. invalid_models.fielderrors: "choices": "choices" should be iterable (e.g., a tuple or list). -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. -invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-tuples. +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). +invalid_models.fielderrors: "choices2": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples). invalid_models.fielderrors: "index": "db_index" should be either None, True or False. invalid_models.fielderrors: "field_": Field names cannot end with underscores, because this would lead to ambiguous queryset filters. invalid_models.fielderrors: "nullbool": BooleanFields do not accept null values. Use a NullBooleanField instead. +invalid_models.fielderrors: "generic_ip_notnull_blank": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null. invalid_models.clash1: Accessor for field 'foreign' clashes with field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'. invalid_models.clash1: Accessor for field 'foreign' clashes with related m2m field 'Target.clash1_set'. Add a related_name argument to the definition for 'foreign'. invalid_models.clash1: Reverse query name for field 'foreign' clashes with field 'Target.clash1'. Add a related_name argument to the definition for 'foreign'. diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py index 27ae651042..0c2d269464 100644 --- a/tests/logging_tests/tests.py +++ b/tests/logging_tests/tests.py @@ -8,9 +8,10 @@ import warnings from django.conf import LazySettings from django.core import mail from django.test import TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, patch_logger from django.utils.encoding import force_text -from django.utils.log import CallbackFilter, RequireDebugFalse, RequireDebugTrue +from django.utils.log import (CallbackFilter, RequireDebugFalse, + RequireDebugTrue) from django.utils.six import StringIO from django.utils.unittest import skipUnless @@ -293,7 +294,7 @@ class AdminEmailHandlerTest(TestCase): def my_mail_admins(*args, **kwargs): connection = kwargs['connection'] - self.assertTrue(isinstance(connection, MyEmailBackend)) + self.assertIsInstance(connection, MyEmailBackend) mail_admins_called['called'] = True # Monkeypatches @@ -354,3 +355,22 @@ class SettingsConfigureLogging(TestCase): settings.configure( LOGGING_CONFIG='logging_tests.tests.dictConfig') self.assertTrue(dictConfig.called) + + +class SecurityLoggerTest(TestCase): + + urls = 'logging_tests.urls' + + def test_suspicious_operation_creates_log_message(self): + with self.settings(DEBUG=True): + with patch_logger('django.security.SuspiciousOperation', 'error') as calls: + response = self.client.get('/suspicious/') + self.assertEqual(len(calls), 1) + self.assertEqual(calls[0], 'dubious') + + def test_suspicious_operation_uses_sublogger(self): + with self.settings(DEBUG=True): + with patch_logger('django.security.DisallowedHost', 'error') as calls: + response = self.client.get('/suspicious_spec/') + self.assertEqual(len(calls), 1) + self.assertEqual(calls[0], 'dubious') diff --git a/tests/logging_tests/urls.py b/tests/logging_tests/urls.py new file mode 100644 index 0000000000..c738bd565c --- /dev/null +++ b/tests/logging_tests/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals + +from django.conf.urls import patterns, url + +from . import views + +urlpatterns = patterns('', + url(r'^suspicious/$', views.suspicious), + url(r'^suspicious_spec/$', views.suspicious_spec), +) diff --git a/tests/logging_tests/views.py b/tests/logging_tests/views.py new file mode 100644 index 0000000000..c685bcc005 --- /dev/null +++ b/tests/logging_tests/views.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.core.exceptions import SuspiciousOperation, DisallowedHost + + +def suspicious(request): + raise SuspiciousOperation('dubious') + + +def suspicious_spec(request): + raise DisallowedHost('dubious') diff --git a/tests/mail/tests.py b/tests/mail/tests.py index 0a843db9e7..c90dc7e22a 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -251,16 +251,16 @@ class MailTests(TestCase): def test_backend_arg(self): """Test backend argument of mail.get_connection()""" - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.smtp.EmailBackend'), smtp.EmailBackend)) - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.locmem.EmailBackend'), locmem.EmailBackend)) - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.dummy.EmailBackend'), dummy.EmailBackend)) - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.console.EmailBackend'), console.EmailBackend)) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.smtp.EmailBackend'), smtp.EmailBackend) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.locmem.EmailBackend'), locmem.EmailBackend) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.dummy.EmailBackend'), dummy.EmailBackend) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.console.EmailBackend'), console.EmailBackend) tmp_dir = tempfile.mkdtemp() try: - self.assertTrue(isinstance(mail.get_connection('django.core.mail.backends.filebased.EmailBackend', file_path=tmp_dir), filebased.EmailBackend)) + self.assertIsInstance(mail.get_connection('django.core.mail.backends.filebased.EmailBackend', file_path=tmp_dir), filebased.EmailBackend) finally: shutil.rmtree(tmp_dir) - self.assertTrue(isinstance(mail.get_connection(), locmem.EmailBackend)) + self.assertIsInstance(mail.get_connection(), locmem.EmailBackend) @override_settings( EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend', diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index f2f7f4df66..265eb97c36 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -18,13 +18,11 @@ from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware from django.middleware.transaction import TransactionMiddleware from django.test import TransactionTestCase, TestCase, RequestFactory -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.encoding import force_str from django.utils.six.moves import xrange -from django.utils.unittest import expectedFailure - -from transactions.tests import IgnorePendingDeprecationWarningsMixin +from django.utils.unittest import expectedFailure, skipIf from .models import Band @@ -320,6 +318,33 @@ class BrokenLinkEmailsMiddlewareTest(TestCase): BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) self.assertEqual(len(mail.outbox), 0) + @skipIf(six.PY3, "HTTP_REFERER is str type on Python 3") + def test_404_error_nonascii_referrer(self): + # Such referer strings should not happen, but anyway, if it happens, + # let's not crash + self.req.META['HTTP_REFERER'] = b'http://testserver/c/\xd0\xbb\xd0\xb8/' + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) + + def test_custom_request_checker(self): + class SubclassedMiddleware(BrokenLinkEmailsMiddleware): + ignored_user_agent_patterns = (re.compile(r'Spider.*'), + re.compile(r'Robot.*')) + def is_ignorable_request(self, request, uri, domain, referer): + '''Check user-agent in addition to normal checks.''' + if super(SubclassedMiddleware, self).is_ignorable_request(request, uri, domain, referer): + return True + user_agent = request.META['HTTP_USER_AGENT'] + return any(pattern.search(user_agent) for pattern in + self.ignored_user_agent_patterns) + + self.req.META['HTTP_REFERER'] = '/another/url/' + self.req.META['HTTP_USER_AGENT'] = 'Spider machine 3.4' + SubclassedMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 0) + self.req.META['HTTP_USER_AGENT'] = 'My user agent' + SubclassedMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) class ConditionalGetMiddlewareTest(TestCase): urls = 'middleware.cond_get_urls' diff --git a/tests/model_fields/tests.py b/tests/model_fields/tests.py index e1e38d0ec7..ccff8b8cfa 100644 --- a/tests/model_fields/tests.py +++ b/tests/model_fields/tests.py @@ -193,28 +193,28 @@ class BooleanFieldTests(unittest.TestCase): b.bfield = True b.save() b2 = BooleanModel.objects.get(pk=b.pk) - self.assertTrue(isinstance(b2.bfield, bool)) + self.assertIsInstance(b2.bfield, bool) self.assertEqual(b2.bfield, True) b3 = BooleanModel() b3.bfield = False b3.save() b4 = BooleanModel.objects.get(pk=b3.pk) - self.assertTrue(isinstance(b4.bfield, bool)) + self.assertIsInstance(b4.bfield, bool) self.assertEqual(b4.bfield, False) b = NullBooleanModel() b.nbfield = True b.save() b2 = NullBooleanModel.objects.get(pk=b.pk) - self.assertTrue(isinstance(b2.nbfield, bool)) + self.assertIsInstance(b2.nbfield, bool) self.assertEqual(b2.nbfield, True) b3 = NullBooleanModel() b3.nbfield = False b3.save() b4 = NullBooleanModel.objects.get(pk=b3.pk) - self.assertTrue(isinstance(b4.nbfield, bool)) + self.assertIsInstance(b4.nbfield, bool) self.assertEqual(b4.nbfield, False) # http://code.djangoproject.com/ticket/13293 @@ -371,11 +371,11 @@ class BigIntegerFieldTests(test.TestCase): def test_types(self): b = BigInt(value = 0) - self.assertTrue(isinstance(b.value, six.integer_types)) + self.assertIsInstance(b.value, six.integer_types) b.save() - self.assertTrue(isinstance(b.value, six.integer_types)) + self.assertIsInstance(b.value, six.integer_types) b = BigInt.objects.all()[0] - self.assertTrue(isinstance(b.value, six.integer_types)) + self.assertIsInstance(b.value, six.integer_types) def test_coercing(self): BigInt.objects.create(value ='10') diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index a79d9b8c5b..610dc34001 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -207,7 +207,17 @@ class Post(models.Model): posted = models.DateField() def __str__(self): - return self.name + return self.title + +@python_2_unicode_compatible +class DateTimePost(models.Model): + title = models.CharField(max_length=50, unique_for_date='posted', blank=True) + slug = models.CharField(max_length=50, unique_for_year='posted', blank=True) + subtitle = models.CharField(max_length=50, unique_for_month='posted', blank=True) + posted = models.DateTimeField(editable=False) + + def __str__(self): + return self.title class DerivedPost(Post): pass @@ -255,3 +265,7 @@ class Colour(models.Model): class ColourfulItem(models.Model): name = models.CharField(max_length=50) colours = models.ManyToManyField(Colour) + +class ArticleStatusNote(models.Model): + name = models.CharField(max_length=20) + status = models.ManyToManyField(ArticleStatus) diff --git a/tests/model_forms/tests.py b/tests/model_forms/tests.py index c5db011404..58dde13a8a 100644 --- a/tests/model_forms/tests.py +++ b/tests/model_forms/tests.py @@ -24,7 +24,7 @@ from .models import (Article, ArticleStatus, BetterAuthor, BigInt, DerivedPost, ExplicitPK, FlexibleDatePost, ImprovedArticle, ImprovedArticleWithParentLink, Inventory, Post, Price, Product, TextFile, AuthorProfile, Colour, ColourfulItem, - test_images) + ArticleStatusNote, DateTimePost, test_images) if test_images: from .models import ImageFile, OptionalImageFile @@ -76,6 +76,12 @@ class PostForm(forms.ModelForm): fields = '__all__' +class DateTimePostForm(forms.ModelForm): + class Meta: + model = DateTimePost + fields = '__all__' + + class DerivedPostForm(forms.ModelForm): class Meta: model = DerivedPost @@ -234,6 +240,20 @@ class ColourfulItemForm(forms.ModelForm): model = ColourfulItem fields = '__all__' +# model forms for testing work on #9321: + +class StatusNoteForm(forms.ModelForm): + class Meta: + model = ArticleStatusNote + fields = '__all__' + + +class StatusNoteCBM2mForm(forms.ModelForm): + class Meta: + model = ArticleStatusNote + fields = '__all__' + widgets = {'status': forms.CheckboxSelectMultiple} + class ModelFormBaseTest(TestCase): def test_base_form(self): @@ -274,8 +294,8 @@ class ModelFormBaseTest(TestCase): model = Category fields = '__all__' - self.assertTrue(isinstance(ReplaceField.base_fields['url'], - forms.fields.BooleanField)) + self.assertIsInstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField) def test_replace_field_variant_2(self): # Should have the same result as before, @@ -287,8 +307,8 @@ class ModelFormBaseTest(TestCase): model = Category fields = ['url'] - self.assertTrue(isinstance(ReplaceField.base_fields['url'], - forms.fields.BooleanField)) + self.assertIsInstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField) def test_replace_field_variant_3(self): # Should have the same result as before, @@ -300,8 +320,8 @@ class ModelFormBaseTest(TestCase): model = Category fields = [] # url will still appear, since it is explicit above - self.assertTrue(isinstance(ReplaceField.base_fields['url'], - forms.fields.BooleanField)) + self.assertIsInstance(ReplaceField.base_fields['url'], + forms.fields.BooleanField) def test_override_field(self): class AuthorForm(forms.ModelForm): @@ -668,6 +688,23 @@ class UniqueTest(TestCase): self.assertEqual(len(form.errors), 1) self.assertEqual(form.errors['posted'], ['This field is required.']) + def test_unique_for_date_in_exclude(self): + """If the date for unique_for_* constraints is excluded from the + ModelForm (in this case 'posted' has editable=False, then the + constraint should be ignored.""" + p = DateTimePost.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", + posted=datetime.datetime(2008, 9, 3, 10, 10, 1)) + # 'title' has unique_for_date='posted' + form = DateTimePostForm({'title': "Django 1.0 is released", 'posted': '2008-09-03'}) + self.assertTrue(form.is_valid()) + # 'slug' has unique_for_year='posted' + form = DateTimePostForm({'slug': "Django 1.0", 'posted': '2008-01-01'}) + self.assertTrue(form.is_valid()) + # 'subtitle' has unique_for_month='posted' + form = DateTimePostForm({'subtitle': "Finally", 'posted': '2008-09-30'}) + self.assertTrue(form.is_valid()) + def test_inherited_unique_for_date(self): p = Post.objects.create(title="Django 1.0 is released", slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) @@ -1677,3 +1714,22 @@ class OldFormForXTests(TestCase): <option value="%(blue_pk)s">Blue</option> </select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></p>""" % {'blue_pk': colour.pk}) + + +class M2mHelpTextTest(TestCase): + """Tests for ticket #9321.""" + def test_multiple_widgets(self): + """Help text of different widgets for ManyToManyFields model fields""" + dreaded_help_text = '<span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span>' + + # Default widget (SelectMultiple): + std_form = StatusNoteForm() + self.assertInHTML(dreaded_help_text, std_form.as_p()) + + # Overridden widget (CheckboxSelectMultiple, a subclass of + # SelectMultiple but with a UI that doesn't involve Control/Command + # keystrokes to extend selection): + form = StatusNoteCBM2mForm() + html = form.as_p() + self.assertInHTML('<ul id="id_status">', html) + self.assertInHTML(dreaded_help_text, html, count=0) diff --git a/tests/model_forms_regress/tests.py b/tests/model_forms_regress/tests.py index 0e033e033f..80900a59e0 100644 --- a/tests/model_forms_regress/tests.py +++ b/tests/model_forms_regress/tests.py @@ -92,6 +92,41 @@ class OverrideCleanTests(TestCase): self.assertEqual(form.instance.left, 1) + +class PartiallyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = ('left', 'right',) + + +class FullyLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "__all__" + +class LocalizedModelFormTest(TestCase): + def test_model_form_applies_localize_to_some_fields(self): + f = PartiallyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertFalse(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_applies_localize_to_all_fields(self): + f = FullyLocalizedTripleForm({'left': 10, 'middle': 10, 'right': 10}) + self.assertTrue(f.is_valid()) + self.assertTrue(f.fields['left'].localize) + self.assertTrue(f.fields['middle'].localize) + self.assertTrue(f.fields['right'].localize) + + def test_model_form_refuses_arbitrary_string(self): + with self.assertRaises(TypeError): + class BrokenLocalizedTripleForm(forms.ModelForm): + class Meta: + model = Triple + localized_fields = "foo" + + # Regression test for #12960. # Make sure the cleaned_data returned from ModelForm.clean() is applied to the # model instance. diff --git a/tests/model_formsets_regress/tests.py b/tests/model_formsets_regress/tests.py index 38ebd9d24b..179f79fbcb 100644 --- a/tests/model_formsets_regress/tests.py +++ b/tests/model_formsets_regress/tests.py @@ -236,11 +236,11 @@ class FormsetTests(TestCase): formset = Formset(data) # check if the returned error classes are correct # note: formset.errors returns a list as documented - self.assertTrue(isinstance(formset.errors, list)) - self.assertTrue(isinstance(formset.non_form_errors(), ErrorList)) + self.assertIsInstance(formset.errors, list) + self.assertIsInstance(formset.non_form_errors(), ErrorList) for form in formset.forms: - self.assertTrue(isinstance(form.errors, ErrorDict)) - self.assertTrue(isinstance(form.non_field_errors(), ErrorList)) + self.assertIsInstance(form.errors, ErrorDict) + self.assertIsInstance(form.non_field_errors(), ErrorList) def test_initial_data(self): User.objects.create(username="bibi", serial=1) @@ -273,6 +273,7 @@ class UserSiteForm(forms.ModelForm): 'id': CustomWidget, 'data': CustomWidget, } + localized_fields = ('data',) class Callback(object): @@ -295,21 +296,25 @@ class FormfieldCallbackTests(TestCase): def test_inlineformset_factory_default(self): Formset = inlineformset_factory(User, UserSite, form=UserSiteForm, fields="__all__") form = Formset().forms[0] - self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) - self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertIsInstance(form['id'].field.widget, CustomWidget) + self.assertIsInstance(form['data'].field.widget, CustomWidget) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def test_modelformset_factory_default(self): Formset = modelformset_factory(UserSite, form=UserSiteForm) form = Formset().forms[0] - self.assertTrue(isinstance(form['id'].field.widget, CustomWidget)) - self.assertTrue(isinstance(form['data'].field.widget, CustomWidget)) + self.assertIsInstance(form['id'].field.widget, CustomWidget) + self.assertIsInstance(form['data'].field.widget, CustomWidget) + self.assertFalse(form.fields['id'].localize) + self.assertTrue(form.fields['data'].localize) def assertCallbackCalled(self, callback): id_field, user_field, data_field = UserSite._meta.fields expected_log = [ (id_field, {'widget': CustomWidget}), (user_field, {}), - (data_field, {'widget': CustomWidget}), + (data_field, {'widget': CustomWidget, 'localize': True}), ] self.assertEqual(callback.log, expected_log) diff --git a/tests/model_validation/__init__.py b/tests/model_validation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/model_validation/models.py b/tests/model_validation/models.py new file mode 100644 index 0000000000..9a2a5f7cd0 --- /dev/null +++ b/tests/model_validation/models.py @@ -0,0 +1,27 @@ +from django.db import models + + +class ThingItem(object): + + def __init__(self, value, display): + self.value = value + self.display = display + + def __iter__(self): + return (x for x in [self.value, self.display]) + + def __len__(self): + return 2 + + +class Things(object): + + def __iter__(self): + return (x for x in [ThingItem(1, 2), ThingItem(3, 4)]) + + +class ThingWithIterableChoices(models.Model): + + # Testing choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + thing = models.CharField(max_length=100, blank=True, choices=Things()) diff --git a/tests/model_validation/tests.py b/tests/model_validation/tests.py new file mode 100644 index 0000000000..ffd0d89412 --- /dev/null +++ b/tests/model_validation/tests.py @@ -0,0 +1,13 @@ +from django.core import management +from django.test import TestCase +from django.utils import six + + +class ModelValidationTest(TestCase): + + def test_models_validate(self): + # All our models should validate properly + # Validation Tests: + # * choices= Iterable of Iterables + # See: https://code.djangoproject.com/ticket/20430 + management.call_command("validate", stdout=six.StringIO()) diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index 0d933bc1f9..805b57c070 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -7,7 +7,6 @@ from django.conf import settings from django.contrib.admin.options import (ModelAdmin, TabularInline, HORIZONTAL, VERTICAL) from django.contrib.admin.sites import AdminSite -from django.contrib.admin.validation import validate from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect from django.contrib.admin import (SimpleListFilter, BooleanFieldListFilter) @@ -65,6 +64,30 @@ class ModelAdminTests(TestCase): self.assertEqual(ma.get_fieldsets(request, self.band), [(None, {'fields': ['name', 'bio', 'sign_date']})]) + def test_get_fieldsets(self): + # Test that get_fieldsets is called when figuring out form fields. + # Refs #18681. + + class BandAdmin(ModelAdmin): + def get_fieldsets(self, request, obj=None): + return [(None, {'fields': ['name', 'bio']})] + + ma = BandAdmin(Band, self.site) + form = ma.get_form(None) + self.assertEqual(form._meta.fields, ['name', 'bio']) + + class InlineBandAdmin(TabularInline): + model = Concert + fk_name = 'main_band' + can_delete = False + + def get_fieldsets(self, request, obj=None): + return [(None, {'fields': ['day', 'transport']})] + + ma = InlineBandAdmin(Band, self.site) + form = ma.get_formset(None).form + self.assertEqual(form._meta.fields, ['day', 'transport']) + def test_field_arguments(self): # If we specify the fields argument, fieldsets_add and fielsets_change should # just stick the fields into a formsets structure and return it. @@ -523,8 +546,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -534,8 +556,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -545,15 +566,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.raw_id_fields\[0\]', 'name' must be either a ForeignKey or ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): raw_id_fields = ('users',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_fieldsets_validation(self): @@ -563,8 +583,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -574,8 +593,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -585,8 +603,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]' does not have exactly two elements.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -596,8 +613,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.fieldsets\[0\]\[1\]' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -607,15 +623,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'fields' key is required in ValidationTestModelAdmin.fieldsets\[0\]\[1\] field options dict.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): fieldsets = (("General", {"fields": ("name",)}),) @@ -624,8 +639,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "Both fieldsets and fields are specified in ValidationTestModelAdmin.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -635,8 +649,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "There are duplicate field\(s\) in ValidationTestModelAdmin.fieldsets", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -646,8 +659,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "There are duplicate field\(s\) in ValidationTestModelAdmin.fields", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -662,8 +674,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "ValidationTestModelAdmin.form does not inherit from BaseModelForm.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -676,7 +687,7 @@ class ValidationTests(unittest.TestCase): }), ) - validate(BandAdmin, Band) + BandAdmin.validate(Band) class AdminBandForm(forms.ModelForm): delete = forms.BooleanField() @@ -690,7 +701,7 @@ class ValidationTests(unittest.TestCase): }), ) - validate(BandAdmin, Band) + BandAdmin.validate(Band) def test_filter_vertical_validation(self): @@ -700,8 +711,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -711,8 +721,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -722,15 +731,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_vertical\[0\]' must be a ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): filter_vertical = ("users",) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_filter_horizontal_validation(self): @@ -740,8 +748,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -751,8 +758,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -762,15 +768,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.filter_horizontal\[0\]' must be a ManyToManyField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): filter_horizontal = ("users",) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_radio_fields_validation(self): @@ -780,8 +785,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -791,8 +795,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -802,8 +805,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields\['name'\]' is neither an instance of ForeignKey nor does have choices set.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -813,15 +815,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.radio_fields\['state'\]' is neither admin.HORIZONTAL nor admin.VERTICAL.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): radio_fields = {"state": VERTICAL} - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_prepopulated_fields_validation(self): @@ -831,8 +832,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields' must be a dictionary.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -842,8 +842,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -853,8 +852,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields\['slug'\]\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -864,15 +862,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.prepopulated_fields\['users'\]' is either a DateTimeField, ForeignKey or ManyToManyField. This isn't allowed.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): prepopulated_fields = {"slug": ("name",)} - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_display_validation(self): @@ -882,8 +879,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -893,8 +889,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, str_prefix("ValidationTestModelAdmin.list_display\[0\], %(_)s'non_existent_field' is not a callable or an attribute of 'ValidationTestModelAdmin' or found in the model 'ValidationTestModel'."), - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -904,8 +899,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display\[0\]', 'users' is a ManyToManyField which is not supported.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -917,7 +911,7 @@ class ValidationTests(unittest.TestCase): pass list_display = ('name', 'decade_published_in', 'a_method', a_callable) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_display_links_validation(self): @@ -927,8 +921,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -938,8 +931,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'non_existent_field' which is not defined in 'list_display'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -949,8 +941,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_display_links\[0\]' refers to 'name' which is not defined in 'list_display'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -963,7 +954,7 @@ class ValidationTests(unittest.TestCase): list_display = ('name', 'decade_published_in', 'a_method', a_callable) list_display_links = ('name', 'decade_published_in', 'a_method', a_callable) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_filter_validation(self): @@ -973,8 +964,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -984,8 +974,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' refers to 'non_existent_field' which does not refer to a Field.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -998,8 +987,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not a descendant of ListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1009,8 +997,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1028,8 +1015,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1039,8 +1025,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1049,7 +1034,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter), 'no') - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_per_page_validation(self): @@ -1058,16 +1043,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_per_page' should be a integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_per_page' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_per_page = 100 - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_max_show_all_allowed_validation(self): @@ -1076,16 +1060,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_max_show_all' should be an integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_max_show_all' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_max_show_all = 200 - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_search_fields_validation(self): @@ -1095,8 +1078,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.search_fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1108,8 +1090,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.date_hierarchy' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1119,15 +1100,14 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.date_hierarchy is neither an instance of DateField nor DateTimeField.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): date_hierarchy = 'pub_date' - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_ordering_validation(self): @@ -1137,8 +1117,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1148,8 +1127,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering\[0\]' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1159,43 +1137,43 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.ordering' has the random ordering marker '\?', but contains other fields as well. Please either remove '\?' or the other fields.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): ordering = ('?',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): ordering = ('band__name',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) class ValidationTestModelAdmin(ModelAdmin): ordering = ('name',) - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_list_select_related_validation(self): class ValidationTestModelAdmin(ModelAdmin): list_select_related = 1 - six.assertRaisesRegex(self, + six.assertRaisesRegex( + self, ImproperlyConfigured, - "'ValidationTestModelAdmin.list_select_related' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.list_select_related' should be either a " + "bool, a tuple or a list", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): list_select_related = False - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_save_as_validation(self): @@ -1204,16 +1182,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.save_as' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.save_as' should be a bool.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): save_as = True - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_save_on_top_validation(self): @@ -1222,16 +1199,15 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestModelAdmin.save_on_top' should be a boolean.", - validate, - ValidationTestModelAdmin, + "'ValidationTestModelAdmin.save_on_top' should be a bool.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) class ValidationTestModelAdmin(ModelAdmin): save_on_top = True - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_inlines_validation(self): @@ -1241,8 +1217,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1255,8 +1230,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines\[0\]' does not inherit from BaseModelAdmin.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1269,8 +1243,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'model' is a required attribute of 'ValidationTestModelAdmin.inlines\[0\]'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1286,8 +1259,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestModelAdmin.inlines\[0\].model' does not inherit from models.Model.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1297,7 +1269,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_fields_validation(self): @@ -1311,8 +1283,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.fields' must be a list or tuple.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1328,8 +1299,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.fk_name' refers to field 'non_existent_field' that is missing from model 'modeladmin.ValidationTestInlineModel'.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1340,7 +1310,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_extra_validation(self): @@ -1353,9 +1323,8 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestInline.extra' should be a integer.", - validate, - ValidationTestModelAdmin, + "'ValidationTestInline.extra' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1366,7 +1335,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_max_num_validation(self): @@ -1379,9 +1348,8 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, - "'ValidationTestInline.max_num' should be an integer or None \(default\).", - validate, - ValidationTestModelAdmin, + "'ValidationTestInline.max_num' should be a int.", + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1392,7 +1360,7 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) def test_formset_validation(self): @@ -1409,8 +1377,7 @@ class ValidationTests(unittest.TestCase): six.assertRaisesRegex(self, ImproperlyConfigured, "'ValidationTestInline.formset' does not inherit from BaseModelFormSet.", - validate, - ValidationTestModelAdmin, + ValidationTestModelAdmin.validate, ValidationTestModel, ) @@ -1424,4 +1391,4 @@ class ValidationTests(unittest.TestCase): class ValidationTestModelAdmin(ModelAdmin): inlines = [ValidationTestInline] - validate(ValidationTestModelAdmin, ValidationTestModel) + ValidationTestModelAdmin.validate(ValidationTestModel) diff --git a/tests/multiple_database/tests.py b/tests/multiple_database/tests.py index 2bff7b3b66..e72fb6a4f9 100644 --- a/tests/multiple_database/tests.py +++ b/tests/multiple_database/tests.py @@ -5,11 +5,13 @@ import pickle from operator import attrgetter import warnings +from django.conf import settings from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.core import management from django.db import connections, router, DEFAULT_DB_ALIAS from django.db.models import signals +from django.db.utils import ConnectionRouter from django.test import TestCase from django.test.utils import override_settings from django.utils.six import StringIO @@ -918,6 +920,7 @@ class QueryTestCase(TestCase): published=datetime.date(2009, 5, 4), extra_arg=True) + class TestRouter(object): # A test router. The behavior is vaguely master/slave, but the # databases aren't assumed to propagate changes. @@ -972,6 +975,30 @@ class WriteRouter(object): def db_for_write(self, model, **hints): return 'writer' + +class ConnectionRouterTestCase(TestCase): + @override_settings(DATABASE_ROUTERS=[ + 'multiple_database.tests.TestRouter', + 'multiple_database.tests.WriteRouter']) + def test_router_init_default(self): + router = ConnectionRouter() + self.assertListEqual([r.__class__.__name__ for r in router.routers], + ['TestRouter', 'WriteRouter']) + + def test_router_init_arg(self): + router = ConnectionRouter([ + 'multiple_database.tests.TestRouter', + 'multiple_database.tests.WriteRouter' + ]) + self.assertListEqual([r.__class__.__name__ for r in router.routers], + ['TestRouter', 'WriteRouter']) + + # Init with instances instead of strings + router = ConnectionRouter([TestRouter(), WriteRouter()]) + self.assertListEqual([r.__class__.__name__ for r in router.routers], + ['TestRouter', 'WriteRouter']) + + class RouterTestCase(TestCase): multi_db = True diff --git a/tests/one_to_one/tests.py b/tests/one_to_one/tests.py index 6ee78520bd..a36764b788 100644 --- a/tests/one_to_one/tests.py +++ b/tests/one_to_one/tests.py @@ -22,7 +22,8 @@ class OneToOneTests(TestCase): # A Place can access its restaurant, if available. self.assertEqual(repr(self.p1.restaurant), '<Restaurant: Demon Dogs the restaurant>') # p2 doesn't have an associated restaurant. - self.assertRaises(Restaurant.DoesNotExist, getattr, self.p2, 'restaurant') + with self.assertRaisesMessage(Restaurant.DoesNotExist, 'Place has no restaurant'): + self.p2.restaurant def test_setter(self): # Set the place using assignment notation. Because place is the primary diff --git a/tests/pagination/tests.py b/tests/pagination/tests.py index dea5756672..1dea4526e3 100644 --- a/tests/pagination/tests.py +++ b/tests/pagination/tests.py @@ -297,6 +297,7 @@ class ModelPaginationTests(TestCase): self.assertIsNone(p.object_list._result_cache) self.assertRaises(TypeError, lambda: p['has_previous']) self.assertIsNone(p.object_list._result_cache) + self.assertNotIsInstance(p.object_list, list) # Make sure slicing the Page object with numbers and slice objects work. self.assertEqual(p[0], Article.objects.get(headline='Article 1')) @@ -305,3 +306,5 @@ class ModelPaginationTests(TestCase): "<Article: Article 2>", ] ) + # After __getitem__ is called, object_list is a list + self.assertIsInstance(p.object_list, list) diff --git a/tests/prefetch_related/models.py b/tests/prefetch_related/models.py index 81c569844f..82bf85e401 100644 --- a/tests/prefetch_related/models.py +++ b/tests/prefetch_related/models.py @@ -195,3 +195,23 @@ class Employee(models.Model): class Meta: ordering = ['id'] + + +### Ticket 19607 + +@python_2_unicode_compatible +class LessonEntry(models.Model): + name1 = models.CharField(max_length=200) + name2 = models.CharField(max_length=200) + + def __str__(self): + return "%s %s" % (self.name1, self.name2) + + +@python_2_unicode_compatible +class WordEntry(models.Model): + lesson_entry = models.ForeignKey(LessonEntry) + name = models.CharField(max_length=200) + + def __str__(self): + return "%s (%s)" % (self.name, self.id) diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index e81560f01f..2e3fee6be6 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -8,7 +8,8 @@ from django.utils import six from .models import (Author, Book, Reader, Qualification, Teacher, Department, TaggedItem, Bookmark, AuthorAddress, FavoriteAuthors, AuthorWithAge, - BookWithYear, BookReview, Person, House, Room, Employee, Comment) + BookWithYear, BookReview, Person, House, Room, Employee, Comment, + LessonEntry, WordEntry) class PrefetchRelatedTests(TestCase): @@ -106,6 +107,16 @@ class PrefetchRelatedTests(TestCase): qs = Book.objects.prefetch_related('first_time_authors') [b.first_time_authors.exists() for b in qs] + def test_in_and_prefetch_related(self): + """ + Regression test for #20242 - QuerySet "in" didn't work the first time + when using prefetch_related. This was fixed by the removal of chunked + reads from QuerySet iteration in + 70679243d1786e03557c28929f9762a119e3ac14. + """ + qs = Book.objects.prefetch_related('first_time_authors') + self.assertTrue(qs[0] in qs) + def test_clear(self): """ Test that we can clear the behavior by calling prefetch_related() @@ -618,3 +629,25 @@ class MultiDbTests(TestCase): ages = ", ".join(str(a.authorwithage.age) for a in A.prefetch_related('authorwithage')) self.assertEqual(ages, "50, 49") + + +class Ticket19607Tests(TestCase): + + def setUp(self): + + for id, name1, name2 in [ + (1, 'einfach', 'simple'), + (2, 'schwierig', 'difficult'), + ]: + LessonEntry.objects.create(id=id, name1=name1, name2=name2) + + for id, lesson_entry_id, name in [ + (1, 1, 'einfach'), + (2, 1, 'simple'), + (3, 2, 'schwierig'), + (4, 2, 'difficult'), + ]: + WordEntry.objects.create(id=id, lesson_entry_id=lesson_entry_id, name=name) + + def test_bug(self): + list(WordEntry.objects.prefetch_related('lesson_entry', 'lesson_entry__wordentry_set')) diff --git a/tests/queries/tests.py b/tests/queries/tests.py index cdc26248c9..481b690c20 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -9,7 +9,6 @@ from django.conf import settings from django.core.exceptions import FieldError from django.db import DatabaseError, connection, connections, DEFAULT_DB_ALIAS from django.db.models import Count, F, Q -from django.db.models.query import ITER_CHUNK_SIZE from django.db.models.sql.where import WhereNode, EverythingNode, NothingNode from django.db.models.sql.datastructures import EmptyResultSet from django.test import TestCase, skipUnlessDBFeature @@ -18,7 +17,7 @@ from django.utils import unittest from django.utils.datastructures import SortedDict from .models import (Annotation, Article, Author, Celebrity, Child, Cover, - Detail, DumbCategory, ExtraInfo, Fan, Item, LeafA, LoopX, LoopZ, + Detail, DumbCategory, ExtraInfo, Fan, Item, LeafA, Join, LeafB, LoopX, LoopZ, ManagedModel, Member, NamedCategory, Note, Number, Plaything, PointerA, Ranking, Related, Report, ReservedName, Tag, TvChef, Valid, X, Food, Eaten, Node, ObjectA, ObjectB, ObjectC, CategoryItem, SimpleCategory, @@ -1112,6 +1111,17 @@ class Queries1Tests(BaseQuerysetTest): ['<Report: r1>'] ) + def test_ticket_20250(self): + # A negated Q along with an annotated queryset failed in Django 1.4 + qs = Author.objects.annotate(Count('item')) + qs = qs.filter(~Q(extra__value=0)) + + self.assertTrue('SELECT' in str(qs.query)) + self.assertQuerysetEqual( + qs, + ['<Author: a1>', '<Author: a2>', '<Author: a3>', '<Author: a4>'] + ) + class Queries2Tests(TestCase): def setUp(self): @@ -1211,16 +1221,6 @@ class Queries2Tests(TestCase): ordered=False ) - def test_ticket7411(self): - # Saving to db must work even with partially read result set in another - # cursor. - for num in range(2 * ITER_CHUNK_SIZE + 1): - _ = Number.objects.create(num=num) - - for i, obj in enumerate(Number.objects.all()): - obj.save() - if i > 10: break - def test_ticket7759(self): # Count should work with a partially read result set. count = Number.objects.count() @@ -1700,31 +1700,6 @@ class Queries6Tests(TestCase): ann1.notes.add(n1) ann2 = Annotation.objects.create(name='a2', tag=t4) - # This next test used to cause really weird PostgreSQL behavior, but it was - # only apparent much later when the full test suite ran. - # - Yeah, it leaves global ITER_CHUNK_SIZE to 2 instead of 100... - #@unittest.expectedFailure - def test_slicing_and_cache_interaction(self): - # We can do slicing beyond what is currently in the result cache, - # too. - - # We need to mess with the implementation internals a bit here to decrease the - # cache fill size so that we don't read all the results at once. - from django.db.models import query - query.ITER_CHUNK_SIZE = 2 - qs = Tag.objects.all() - - # Fill the cache with the first chunk. - self.assertTrue(bool(qs)) - self.assertEqual(len(qs._result_cache), 2) - - # Query beyond the end of the cache and check that it is filled out as required. - self.assertEqual(repr(qs[4]), '<Tag: t5>') - self.assertEqual(len(qs._result_cache), 5) - - # But querying beyond the end of the result set will fail. - self.assertRaises(IndexError, lambda: qs[100]) - def test_parallel_iterators(self): # Test that parallel iterators work. qs = Tag.objects.all() @@ -2533,6 +2508,21 @@ class WhereNodeTest(TestCase): w = WhereNode(children=[empty_w, NothingNode()], connector='OR') self.assertRaises(EmptyResultSet, w.as_sql, qn, connection) + +class IteratorExceptionsTest(TestCase): + def test_iter_exceptions(self): + qs = ExtraInfo.objects.only('author') + with self.assertRaises(AttributeError): + list(qs) + + def test_invalid_qs_list(self): + # Test for #19895 - second iteration over invalid queryset + # raises errors. + qs = Article.objects.order_by('invalid_column') + self.assertRaises(FieldError, list, qs) + self.assertRaises(FieldError, list, qs) + + class NullJoinPromotionOrTest(TestCase): def setUp(self): self.d1 = ModelD.objects.create(name='foo') @@ -2831,3 +2821,45 @@ class EmptyStringPromotionTests(TestCase): self.assertIn('LEFT OUTER JOIN', str(qs.query)) else: self.assertNotIn('LEFT OUTER JOIN', str(qs.query)) + +class ValuesSubqueryTests(TestCase): + def test_values_in_subquery(self): + # Check that if a values() queryset is used, then the given values + # will be used instead of forcing use of the relation's field. + o1 = Order.objects.create(id=-2) + o2 = Order.objects.create(id=-1) + oi1 = OrderItem.objects.create(order=o1, status=0) + oi1.status = oi1.pk + oi1.save() + OrderItem.objects.create(order=o2, status=0) + + # The query below should match o1 as it has related order_item + # with id == status. + self.assertQuerysetEqual( + Order.objects.filter(items__in=OrderItem.objects.values_list('status')), + [o1.pk], lambda x: x.pk) + +class DoubleInSubqueryTests(TestCase): + def test_double_subquery_in(self): + lfa1 = LeafA.objects.create(data='foo') + lfa2 = LeafA.objects.create(data='bar') + lfb1 = LeafB.objects.create(data='lfb1') + lfb2 = LeafB.objects.create(data='lfb2') + Join.objects.create(a=lfa1, b=lfb1) + Join.objects.create(a=lfa2, b=lfb2) + leaf_as = LeafA.objects.filter(data='foo').values_list('pk', flat=True) + joins = Join.objects.filter(a__in=leaf_as).values_list('b__id', flat=True) + qs = LeafB.objects.filter(pk__in=joins) + self.assertQuerysetEqual( + qs, [lfb1], lambda x: x) + +class Ticket18785Tests(unittest.TestCase): + def test_ticket_18785(self): + # Test join trimming from ticket18785 + qs = Item.objects.exclude( + note__isnull=False + ).filter( + name='something', creator__extra__isnull=True + ).order_by() + self.assertEqual(1, str(qs.query).count('INNER JOIN')) + self.assertEqual(0, str(qs.query).count('OUTER JOIN')) diff --git a/tests/requests/tests.py b/tests/requests/tests.py index daf426ea47..4d730bb561 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -503,9 +503,9 @@ class RequestsTests(SimpleTestCase): }) self.assertEqual(request.POST, {'key': ['España']}) - def test_body_after_POST_multipart(self): + def test_body_after_POST_multipart_form_data(self): """ - Reading body after parsing multipart is not allowed + Reading body after parsing multipart/form-data is not allowed """ # Because multipart is used for large amounts fo data i.e. file uploads, # we don't want the data held in memory twice, and we don't want to @@ -524,6 +524,29 @@ class RequestsTests(SimpleTestCase): self.assertEqual(request.POST, {'name': ['value']}) self.assertRaises(Exception, lambda: request.body) + def test_body_after_POST_multipart_related(self): + """ + Reading body after parsing multipart that isn't form-data is allowed + """ + # Ticket #9054 + # There are cases in which the multipart data is related instead of + # being a binary upload, in which case it should still be accessible + # via body. + payload_data = b"\r\n".join([ + b'--boundary', + b'Content-ID: id; name="name"', + b'', + b'value', + b'--boundary--' + b'']) + payload = FakePayload(payload_data) + request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'multipart/related; boundary=boundary', + 'CONTENT_LENGTH': len(payload), + 'wsgi.input': payload}) + self.assertEqual(request.POST, {}) + self.assertEqual(request.body, payload_data) + def test_POST_multipart_with_content_length_zero(self): """ Multipart POST requests with Content-Length >= 0 are valid and need to be handled. @@ -636,6 +659,24 @@ class RequestsTests(SimpleTestCase): with self.assertRaises(UnreadablePostError): request.body + def test_FILES_connection_error(self): + """ + If wsgi.input.read() raises an exception while trying to read() the + FILES, the exception should be identifiable (not a generic IOError). + """ + class ExplodingBytesIO(BytesIO): + def read(self, len=0): + raise IOError("kaboom!") + + payload = b'x' + request = WSGIRequest({'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'multipart/form-data; boundary=foo_', + 'CONTENT_LENGTH': len(payload), + 'wsgi.input': ExplodingBytesIO(payload)}) + + with self.assertRaises(UnreadablePostError): + request.FILES + @skipIf(connection.vendor == 'sqlite' and connection.settings_dict['NAME'] in ('', ':memory:'), diff --git a/tests/responses/__init__.py b/tests/responses/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/responses/models.py b/tests/responses/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/responses/tests.py b/tests/responses/tests.py new file mode 100644 index 0000000000..e5320f5af9 --- /dev/null +++ b/tests/responses/tests.py @@ -0,0 +1,15 @@ +from django.http import HttpResponse +import unittest + +class HttpResponseTests(unittest.TestCase): + + def test_status_code(self): + resp = HttpResponse(status=418) + self.assertEqual(resp.status_code, 418) + self.assertEqual(resp.reason_phrase, "I'M A TEAPOT") + + def test_reason_phrase(self): + reason = "I'm an anarchist coffee pot on crack." + resp = HttpResponse(status=814, reason=reason) + self.assertEqual(resp.status_code, 814) + self.assertEqual(resp.reason_phrase, reason) diff --git a/tests/select_related/tests.py b/tests/select_related/tests.py index 27d65fecb1..baa141d123 100644 --- a/tests/select_related/tests.py +++ b/tests/select_related/tests.py @@ -172,3 +172,7 @@ class SelectRelatedTests(TestCase): Species.objects.select_related, 'genus__family__order', depth=4 ) + + def test_none_clears_list(self): + queryset = Species.objects.select_related('genus').select_related(None) + self.assertEqual(queryset.query.select_related, False) diff --git a/tests/special_headers/models.py b/tests/special_headers/models.py deleted file mode 100644 index e05c5a6920..0000000000 --- a/tests/special_headers/models.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.db import models - - -class Article(models.Model): - text = models.TextField() diff --git a/tests/special_headers/templates/special_headers/article_detail.html b/tests/special_headers/templates/special_headers/article_detail.html deleted file mode 100644 index 3cbd38cb7e..0000000000 --- a/tests/special_headers/templates/special_headers/article_detail.html +++ /dev/null @@ -1 +0,0 @@ -{{ object }} diff --git a/tests/special_headers/tests.py b/tests/special_headers/tests.py deleted file mode 100644 index b4b704ae21..0000000000 --- a/tests/special_headers/tests.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.contrib.auth.models import User -from django.test import TestCase -from django.test.utils import override_settings - - -@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class SpecialHeadersTest(TestCase): - fixtures = ['data.xml'] - urls = 'special_headers.urls' - - def test_xheaders(self): - user = User.objects.get(username='super') - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - self.client.login(username='super', password='secret') - response = self.client.get('/special_headers/article/1/') - self.assertTrue('X-Object-Type' in response) - user.is_staff = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.get('/special_headers/article/1/') - self.assertFalse('X-Object-Type' in response) - - def test_xview_func(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/func/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.xview') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/func/') - self.assertFalse('X-View' in response) - - def test_xview_class(self): - user = User.objects.get(username='super') - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - self.client.login(username='super', password='secret') - response = self.client.head('/special_headers/xview/class/') - self.assertTrue('X-View' in response) - self.assertEqual(response['X-View'], 'special_headers.views.XViewClass') - user.is_staff = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) - user.is_staff = True - user.is_active = False - user.save() - response = self.client.head('/special_headers/xview/class/') - self.assertFalse('X-View' in response) diff --git a/tests/special_headers/urls.py b/tests/special_headers/urls.py deleted file mode 100644 index f7ba141207..0000000000 --- a/tests/special_headers/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -# coding: utf-8 -from __future__ import absolute_import - -from django.conf.urls import patterns - -from . import views -from .models import Article - -urlpatterns = patterns('', - (r'^special_headers/article/(?P<object_id>\d+)/$', views.xview_xheaders), - (r'^special_headers/xview/func/$', views.xview_dec(views.xview)), - (r'^special_headers/xview/class/$', views.xview_dec(views.XViewClass.as_view())), -) diff --git a/tests/string_lookup/tests.py b/tests/string_lookup/tests.py index 02f766adce..b011720ddf 100644 --- a/tests/string_lookup/tests.py +++ b/tests/string_lookup/tests.py @@ -73,9 +73,11 @@ class StringLookupTests(TestCase): """ Regression test for #708 - "like" queries on IP address fields require casting to text (on PostgreSQL). + "like" queries on IP address fields require casting with HOST() (on PostgreSQL). """ a = Article(name='IP test', text='The body', submitted_from='192.0.2.100') a.save() self.assertEqual(repr(Article.objects.filter(submitted_from__contains='192.0.2')), repr([a])) + # Test that the searches do not match the subnet mask (/32 in this case) + self.assertEqual(Article.objects.filter(submitted_from__contains='32').count(), 0) \ No newline at end of file diff --git a/tests/syncdb_signals/__init__.py b/tests/syncdb_signals/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/syncdb_signals/models.py b/tests/syncdb_signals/models.py new file mode 100644 index 0000000000..c41d993e94 --- /dev/null +++ b/tests/syncdb_signals/models.py @@ -0,0 +1,11 @@ +# from django.db import models + + +# class Author(models.Model): +# name = models.CharField(max_length=100) + +# class Meta: +# ordering = ['name'] + +# def __unicode__(self): +# return self.name diff --git a/tests/syncdb_signals/tests.py b/tests/syncdb_signals/tests.py new file mode 100644 index 0000000000..d9be3b65d4 --- /dev/null +++ b/tests/syncdb_signals/tests.py @@ -0,0 +1,74 @@ +from django.db.models import signals +from django.test import TestCase +from django.core import management +from django.utils import six + +from shared_models import models + + +PRE_SYNCDB_ARGS = ['app', 'create_models', 'verbosity', 'interactive', 'db'] +SYNCDB_DATABASE = 'default' +SYNCDB_VERBOSITY = 1 +SYNCDB_INTERACTIVE = False + + +class PreSyncdbReceiver(object): + def __init__(self): + self.call_counter = 0 + self.call_args = None + + def __call__(self, signal, sender, **kwargs): + self.call_counter = self.call_counter + 1 + self.call_args = kwargs + + +class OneTimeReceiver(object): + """ + Special receiver for handle the fact that test runner calls syncdb for + several databases and several times for some of them. + """ + + def __init__(self): + self.call_counter = 0 + self.call_args = None + + def __call__(self, signal, sender, **kwargs): + # Although test runner calls syncdb for several databases, + # testing for only one of them is quite sufficient. + if kwargs['db'] == SYNCDB_DATABASE: + self.call_counter = self.call_counter + 1 + self.call_args = kwargs + # we need to test only one call of syncdb + signals.pre_syncdb.disconnect(pre_syncdb_receiver, sender=models) + + +# We connect receiver here and not in unit test code because we need to +# connect receiver before test runner creates database. That is, sequence of +# actions would be: +# +# 1. Test runner imports this module. +# 2. We connect receiver. +# 3. Test runner calls syncdb for create default database. +# 4. Test runner execute our unit test code. +pre_syncdb_receiver = OneTimeReceiver() +signals.pre_syncdb.connect(pre_syncdb_receiver, sender=models) + + +class SyncdbSignalTests(TestCase): + def test_pre_syncdb_call_time(self): + self.assertEqual(pre_syncdb_receiver.call_counter, 1) + + def test_pre_syncdb_args(self): + r = PreSyncdbReceiver() + signals.pre_syncdb.connect(r, sender=models) + management.call_command('syncdb', database=SYNCDB_DATABASE, + verbosity=SYNCDB_VERBOSITY, interactive=SYNCDB_INTERACTIVE, + load_initial_data=False, stdout=six.StringIO()) + + args = r.call_args + self.assertEqual(r.call_counter, 1) + self.assertEqual(set(args), set(PRE_SYNCDB_ARGS)) + self.assertEqual(args['app'], models) + self.assertEqual(args['verbosity'], SYNCDB_VERBOSITY) + self.assertEqual(args['interactive'], SYNCDB_INTERACTIVE) + self.assertEqual(args['db'], 'default') diff --git a/tests/template_tests/filters.py b/tests/template_tests/filters.py index 7ba1681fd5..68ef15d827 100644 --- a/tests/template_tests/filters.py +++ b/tests/template_tests/filters.py @@ -35,59 +35,60 @@ def get_filter_tests(): now_tz_i = datetime.now(FixedOffset((3 * 60) + 15)) # imaginary time zone today = date.today() + # NOTE: \xa0 avoids wrapping between value and unit return { # Default compare with datetime.now() - 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'), - 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'), - 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'), + 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1\xa0minute'), + 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1\xa0day'), + 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1\xa0hour, 25\xa0minutes'), # Compare to a given parameter - 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1 day'), - 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1 minute'), + 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=1)}, '1\xa0day'), + 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now - timedelta(days=2, minutes=1), 'b':now - timedelta(days=2)}, '1\xa0minute'), # Check that timezone is respected - 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8 hours'), + 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz - timedelta(hours=8), 'b':now_tz}, '8\xa0hours'), # Regression for #7443 - 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1 week'), - 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1 week'), - 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0 minutes'), - 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0 minutes'), + 'filter-timesince07': ('{{ earlier|timesince }}', { 'earlier': now - timedelta(days=7) }, '1\xa0week'), + 'filter-timesince08': ('{{ earlier|timesince:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '1\xa0week'), + 'filter-timesince09': ('{{ later|timesince }}', { 'later': now + timedelta(days=7) }, '0\xa0minutes'), + 'filter-timesince10': ('{{ later|timesince:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '0\xa0minutes'), # Ensures that differing timezones are calculated correctly - 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0 minutes'), - 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0 minutes'), - 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0 minutes'), - 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0 minutes'), + 'filter-timesince11' : ('{{ a|timesince }}', {'a': now}, '0\xa0minutes'), + 'filter-timesince12' : ('{{ a|timesince }}', {'a': now_tz}, '0\xa0minutes'), + 'filter-timesince13' : ('{{ a|timesince }}', {'a': now_tz_i}, '0\xa0minutes'), + 'filter-timesince14' : ('{{ a|timesince:b }}', {'a': now_tz, 'b': now_tz_i}, '0\xa0minutes'), 'filter-timesince15' : ('{{ a|timesince:b }}', {'a': now, 'b': now_tz_i}, ''), 'filter-timesince16' : ('{{ a|timesince:b }}', {'a': now_tz_i, 'b': now}, ''), # Regression for #9065 (two date objects). - 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0 minutes'), - 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1 day'), + 'filter-timesince17' : ('{{ a|timesince:b }}', {'a': today, 'b': today}, '0\xa0minutes'), + 'filter-timesince18' : ('{{ a|timesince:b }}', {'a': today, 'b': today + timedelta(hours=24)}, '1\xa0day'), # Default compare with datetime.now() - 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'), - 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'), - 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'), + 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2\xa0minutes'), + 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1\xa0day'), + 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8\xa0hours, 10\xa0minutes'), # Compare to a given parameter - 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'), - 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'), + 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1\xa0day'), + 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1\xa0minute'), # Regression for #7443 - 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0 minutes'), - 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0 minutes'), - 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1 week'), - 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1 week'), + 'filter-timeuntil06': ('{{ earlier|timeuntil }}', { 'earlier': now - timedelta(days=7) }, '0\xa0minutes'), + 'filter-timeuntil07': ('{{ earlier|timeuntil:now }}', { 'now': now, 'earlier': now - timedelta(days=7) }, '0\xa0minutes'), + 'filter-timeuntil08': ('{{ later|timeuntil }}', { 'later': now + timedelta(days=7, hours=1) }, '1\xa0week'), + 'filter-timeuntil09': ('{{ later|timeuntil:now }}', { 'now': now, 'later': now + timedelta(days=7) }, '1\xa0week'), # Ensures that differing timezones are calculated correctly - 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0 minutes'), - 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0 minutes'), + 'filter-timeuntil10' : ('{{ a|timeuntil }}', {'a': now_tz_i}, '0\xa0minutes'), + 'filter-timeuntil11' : ('{{ a|timeuntil:b }}', {'a': now_tz_i, 'b': now_tz}, '0\xa0minutes'), # Regression for #9065 (two date objects). - 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0 minutes'), - 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1 day'), + 'filter-timeuntil12' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today}, '0\xa0minutes'), + 'filter-timeuntil13' : ('{{ a|timeuntil:b }}', {'a': today, 'b': today - timedelta(hours=24)}, '1\xa0day'), 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"<a>\' <a>\'"), 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, r"<a>\' <a>\'"), diff --git a/tests/template_tests/templates/included_base.html b/tests/template_tests/templates/included_base.html new file mode 100644 index 0000000000..eae222cf9d --- /dev/null +++ b/tests/template_tests/templates/included_base.html @@ -0,0 +1,3 @@ +{% block content %} + {% block error_here %}{% endblock %} +{% endblock %} diff --git a/tests/template_tests/templates/included_content.html b/tests/template_tests/templates/included_content.html new file mode 100644 index 0000000000..bfc87c0425 --- /dev/null +++ b/tests/template_tests/templates/included_content.html @@ -0,0 +1,11 @@ +{% extends "included_base.html" %} + +{% block content %} + content + {{ block.super }} +{% endblock %} + +{% block error_here %} + error here + {% url "non_existing_url" %} +{% endblock %} diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 2aeaee9464..206c648398 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -36,6 +36,7 @@ from django.utils.safestring import mark_safe from django.utils import six from django.utils.tzinfo import LocalTimezone +from i18n import TransRealMixin try: from .loaders import RenderToStringTest, EggLoaderTest @@ -154,8 +155,8 @@ class UTF8Class: def __str__(self): return 'ŠĐĆŽćžšđ' -@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/") -class Templates(TestCase): + +class TemplateLoaderTests(TestCase): def test_loaders_security(self): ad_loader = app_directories.Loader() @@ -347,6 +348,9 @@ class Templates(TestCase): loader.template_source_loaders = old_loaders settings.TEMPLATE_DEBUG = old_td + +class TemplateRegressionTests(TestCase): + def test_token_smart_split(self): # Regression test for #7027 token = template.Token(template.TOKEN_BLOCK, 'sometag _("Page not found") value|yesno:_("yes,no")') @@ -444,6 +448,18 @@ class Templates(TestCase): output = template.render(Context({})) self.assertEqual(output, '1st time') + def test_super_errors(self): + """ + Test behavior of the raise errors into included blocks. + See #18169 + """ + t = loader.get_template('included_content.html') + with self.assertRaises(urlresolvers.NoReverseMatch): + t.render(Context({})) + + +@override_settings(MEDIA_URL="/media/", STATIC_URL="/static/") +class TemplateTests(TransRealMixin, TestCase): def test_templates(self): template_tests = self.get_template_tests() filter_tests = filters.get_filter_tests() diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 67c475eaff..bd395ca552 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -21,6 +21,7 @@ urlpatterns = patterns('', (r'^bad_view/$', views.bad_view), (r'^form_view/$', views.form_view), (r'^form_view_with_template/$', views.form_view_with_template), + (r'^formset_view/$', views.formset_view), (r'^login_protected_view/$', views.login_protected_view), (r'^login_protected_method_view/$', views.login_protected_method_view), (r'^login_protected_view_custom_redirect/$', views.login_protected_view_changed_redirect), diff --git a/tests/test_client/views.py b/tests/test_client/views.py index f760466497..76296cb80d 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -7,7 +7,8 @@ from xml.dom.minidom import parseString from django.contrib.auth.decorators import login_required, permission_required from django.core import mail from django.forms import fields -from django.forms.forms import Form +from django.forms.forms import Form, ValidationError +from django.forms.formsets import formset_factory, BaseFormSet from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound from django.shortcuts import render_to_response from django.template import Context, Template @@ -95,6 +96,12 @@ class TestForm(Form): single = fields.ChoiceField(choices=TestChoices) multi = fields.MultipleChoiceField(choices=TestChoices) + def clean(self): + cleaned_data = self.cleaned_data + if cleaned_data.get("text") == "Raise non-field error": + raise ValidationError("Non-field error.") + return cleaned_data + def form_view(request): "A view that tests a simple form" if request.method == 'POST': @@ -130,6 +137,43 @@ def form_view_with_template(request): } ) +class BaseTestFormSet(BaseFormSet): + def clean(self): + """Checks that no two email addresses are the same.""" + if any(self.errors): + # Don't bother validating the formset unless each form is valid + return + + emails = [] + for i in range(0, self.total_form_count()): + form = self.forms[i] + email = form.cleaned_data['email'] + if email in emails: + raise ValidationError( + "Forms in a set must have distinct email addresses." + ) + emails.append(email) + +TestFormSet = formset_factory(TestForm, BaseTestFormSet) + +def formset_view(request): + "A view that tests a simple formset" + if request.method == 'POST': + formset = TestFormSet(request.POST) + if formset.is_valid(): + t = Template('Valid POST data.', name='Valid POST Template') + c = Context() + else: + t = Template('Invalid POST data. {{ my_formset.errors }}', + name='Invalid POST Template') + c = Context({'my_formset': formset}) + else: + formset = TestForm(request.GET) + t = Template('Viewing base formset. {{ my_formset }}.', + name='Formset GET Template') + c = Context({'my_formset': formset}) + return HttpResponse(t.render(c)) + def login_protected_view(request): "A simple view that is login protected." t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') diff --git a/tests/test_client_regress/models.py b/tests/test_client_regress/models.py index e69de29bb2..b72d4480bf 100644 --- a/tests/test_client_regress/models.py +++ b/tests/test_client_regress/models.py @@ -0,0 +1,12 @@ +from django.db import models +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager + + +class CustomUser(AbstractBaseUser): + email = models.EmailField(verbose_name='email address', max_length=255, unique=True) + custom_objects = BaseUserManager() + + USERNAME_FIELD = 'email' + + class Meta: + app_label = 'test_client_regress' diff --git a/tests/test_client_regress/tests.py b/tests/test_client_regress/tests.py index 2582b210c4..67e66fa52d 100644 --- a/tests/test_client_regress/tests.py +++ b/tests/test_client_regress/tests.py @@ -6,10 +6,8 @@ from __future__ import unicode_literals 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, +from django.template import (TemplateSyntaxError, Context, Template, loader) import django.template.context from django.test import Client, TestCase @@ -19,7 +17,11 @@ from django.template.response import SimpleTemplateResponse from django.utils._os import upath from django.utils.translation import ugettext_lazy from django.http import HttpResponse +from django.contrib.auth.signals import user_logged_out, user_logged_in +from django.contrib.auth.models import User +from .models import CustomUser +from .views import CustomTestException @override_settings( TEMPLATE_DIRS=(os.path.join(os.path.dirname(upath(__file__)), 'templates'),) @@ -543,6 +545,197 @@ class AssertFormErrorTests(TestCase): 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)) +class AssertFormsetErrorTests(TestCase): + msg_prefixes = [("", {}), ("abc: ", {"msg_prefix": "abc"})] + def setUp(self): + """Makes response object for testing field and non-field errors""" + # For testing field and non-field errors + self.response_form_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Raise non-field error', + 'form-0-email': 'not an email address', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + # For testing non-form errors + self.response_nonform_errors = self.getResponse({ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-0-text': 'Hello World', + 'form-0-email': 'email@domain.com', + 'form-0-value': 37, + 'form-0-single': 'b', + 'form-0-multi': ('b','c','e'), + 'form-1-text': 'Hello World', + 'form-1-email': 'email@domain.com', + 'form-1-value': 37, + 'form-1-single': 'b', + 'form-1-multi': ('b','c','e'), + }) + + def getResponse(self, post_data): + response = self.client.post('/test_client/formset_view/', post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "Invalid POST Template") + return response + + def test_unknown_formset(self): + "An assertion is raised if the formset name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'wrong_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'wrong_formset' was not " + "used to render the response", + str(cm.exception)) + + def test_unknown_field(self): + "An assertion is raised if the field name is unknown" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'Some_field', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', " + "form 0 in context 0 " + "does not contain the field 'Some_field'", + str(cm.exception)) + + def test_no_error_field(self): + "An assertion is raised if the field doesn't have any errors" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + 'value', + 'Some error.', + **kwargs) + self.assertIn(prefix + "The field 'value' " + "on formset 'my_formset', form 1 " + "in context 0 contains no errors", + str(cm.exception)) + + def test_unknown_error(self): + "An assertion is raised if the field doesn't contain the specified error" + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + "The field 'email' " + "on formset 'my_formset', form 0 in context 0 does not " + "contain the error 'Some error.' (actual errors: " + "[%(_)s'Enter a valid email address.'])"), + str(cm.exception)) + + def test_field_error(self): + "No assertion is raised if the field contains the provided error" + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + 'email', + ['Enter a valid email address.'], + **kwargs) + + def test_no_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 1, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset', form 1 in " + "context 0 does not contain any " + "non-field errors.", + str(cm.exception)) + + def test_unknown_nonfield_error(self): + "An assertion is raised if the formsets non-field errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset', form 0 in context 0 does not " + "contain the non-field error 'Some error.' (actual errors: " + "[%(_)s'Non-field error.'])"), str(cm.exception)) + + def test_nonfield_error(self): + "No assertion is raised if the formsets non-field errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + 0, + None, + 'Non-field error.', + **kwargs) + + def test_no_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain any errors." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_form_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(prefix + "The formset 'my_formset' in context 0 " + "does not contain any non-form errors.", + str(cm.exception)) + + def test_unknown_nonform_error(self): + "An assertion is raised if the formsets non-form errors doesn't contain the provided error." + for prefix, kwargs in self.msg_prefixes: + with self.assertRaises(AssertionError) as cm: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Some error.', + **kwargs) + self.assertIn(str_prefix(prefix + + "The formset 'my_formset' in context 0 does not contain the " + "non-form error 'Some error.' (actual errors: [%(_)s'Forms " + "in a set must have distinct email addresses.'])"), str(cm.exception)) + + def test_nonform_error(self): + "No assertion is raised if the formsets non-form errors contains the provided error." + for prefix, kwargs in self.msg_prefixes: + self.assertFormsetError(self.response_nonform_errors, + 'my_formset', + None, + None, + 'Forms in a set must have distinct email ' + 'addresses.', + **kwargs) + @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) class LoginTests(TestCase): fixtures = ['testdata'] @@ -619,7 +812,7 @@ class ExceptionTests(TestCase): try: response = self.client.get("/test_client_regress/staff_only/") self.fail("General users should not be able to visit this page") - except SuspiciousOperation: + except CustomTestException: pass # At this point, an exception has been raised, and should be cleared. @@ -629,7 +822,7 @@ class ExceptionTests(TestCase): self.assertTrue(login, 'Could not log in') try: self.client.get("/test_client_regress/staff_only/") - except SuspiciousOperation: + except CustomTestException: self.fail("Staff should be able to visit this page") @@ -706,6 +899,21 @@ class ContextTests(TestCase): except KeyError as e: self.assertEqual(e.args[0], 'does-not-exist') + def test_contextlist_keys(self): + c1 = Context() + c1.update({'hello': 'world', 'goodbye': 'john'}) + c1.update({'hello': 'dolly', 'dolly': 'parton'}) + c2 = Context() + c2.update({'goodbye': 'world', 'python': 'rocks'}) + c2.update({'goodbye': 'dolly'}) + + l = ContextList([c1, c2]) + # None, True and False are builtins of BaseContext, and present + # in every Context without needing to be added. + self.assertEqual(set(['None', 'True', 'False', 'hello', 'goodbye', + 'python', 'dolly']), + l.keys()) + 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 @@ -756,6 +964,76 @@ class SessionTests(TestCase): self.client.logout() self.client.logout() + def test_logout_with_user(self): + """Logout should send user_logged_out signal if user was logged in.""" + def listener(*args, **kwargs): + listener.executed = True + self.assertEqual(kwargs['sender'], User) + listener.executed = False + + user_logged_out.connect(listener) + self.client.login(username='testclient', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + self.assertTrue(listener.executed) + + @override_settings(AUTH_USER_MODEL='test_client_regress.CustomUser') + def test_logout_with_custom_user(self): + """Logout should send user_logged_out signal if custom user was logged in.""" + def listener(*args, **kwargs): + self.assertEqual(kwargs['sender'], CustomUser) + listener.executed = True + listener.executed = False + u = CustomUser.custom_objects.create(email='test@test.com') + u.set_password('password') + u.save() + + user_logged_out.connect(listener) + self.client.login(username='test@test.com', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + self.assertTrue(listener.executed) + + def test_logout_without_user(self): + """Logout should send signal even if user not authenticated.""" + def listener(user, *args, **kwargs): + listener.user = user + listener.executed = True + listener.executed = False + + user_logged_out.connect(listener) + self.client.login(username='incorrect', password='password') + self.client.logout() + user_logged_out.disconnect(listener) + + self.assertTrue(listener.executed) + self.assertIsNone(listener.user) + + def test_login_with_user(self): + """Login should send user_logged_in signal on successful login.""" + def listener(*args, **kwargs): + listener.executed = True + listener.executed = False + + user_logged_in.connect(listener) + self.client.login(username='testclient', password='password') + user_logged_out.disconnect(listener) + + self.assertTrue(listener.executed) + + def test_login_without_signal(self): + """Login shouldn't send signal if user wasn't logged in""" + def listener(*args, **kwargs): + listener.executed = True + listener.executed = False + + user_logged_in.connect(listener) + self.client.login(username='incorrect', password='password') + user_logged_in.disconnect(listener) + + self.assertFalse(listener.executed) + + class RequestMethodTests(TestCase): def test_get(self): "Request a view via request method GET" diff --git a/tests/test_client_regress/views.py b/tests/test_client_regress/views.py index 7e86ffd8ca..71e5b526e5 100644 --- a/tests/test_client_regress/views.py +++ b/tests/test_client_regress/views.py @@ -3,12 +3,15 @@ import json from django.conf import settings 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.core.serializers.json import DjangoJSONEncoder from django.test.client import CONTENT_TYPE_RE from django.template import RequestContext + +class CustomTestException(Exception): + pass + def no_template_view(request): "A simple view that expects a GET request, and returns a rendered template" return HttpResponse("No template used. Sample content: twice once twice. Content ends.") @@ -18,7 +21,7 @@ def staff_only_view(request): if request.user.is_staff: return HttpResponse('') else: - raise SuspiciousOperation() + raise CustomTestException() def get_view(request): "A simple login protected view" diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 3dc364b351..1a0fb88367 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -1,5 +1,22 @@ +from contextlib import contextmanager +import os +import sys + from django.test import TestCase from django.test.runner import DiscoverRunner +from django.utils.unittest import expectedFailure + +try: + import unittest2 +except ImportError: + unittest2 = None + + +def expectedFailureIf(condition): + """Marks a test as an expected failure if ``condition`` is met.""" + if condition: + return expectedFailure + return lambda func: func class DiscoverRunnerTest(TestCase): @@ -32,6 +49,9 @@ class DiscoverRunnerTest(TestCase): self.assertEqual(count, 1) + # this test fails if unittest2 is installed from PyPI on Python 2.6 + # refs https://code.djangoproject.com/ticket/20437 + @expectedFailureIf(sys.version_info < (2, 7) and unittest2) def test_dotted_test_method_vanilla_unittest(self): count = DiscoverRunner().build_suite( ["test_discovery_sample.tests_sample.TestVanillaUnittest.test_sample"], @@ -61,8 +81,19 @@ class DiscoverRunnerTest(TestCase): self.assertEqual(count, 1) def test_file_path(self): - count = DiscoverRunner().build_suite( - ["test_discovery_sample/"], - ).countTestCases() + @contextmanager + def change_cwd_to_tests(): + """Change CWD to tests directory (one level up from this file)""" + current_dir = os.path.abspath(os.path.dirname(__file__)) + tests_dir = os.path.join(current_dir, '..') + old_cwd = os.getcwd() + os.chdir(tests_dir) + yield + os.chdir(old_cwd) + + with change_cwd_to_tests(): + count = DiscoverRunner().build_suite( + ["test_discovery_sample/"], + ).countTestCases() self.assertEqual(count, 4) diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index aeb9bc3d2c..0f16a9c805 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -5,6 +5,7 @@ import warnings from django.db import connection, transaction, IntegrityError from django.test import TransactionTestCase, skipUnlessDBFeature +from django.test.utils import IgnorePendingDeprecationWarningsMixin from django.utils import six from django.utils.unittest import skipIf, skipUnless @@ -319,19 +320,6 @@ class AtomicMiscTests(TransactionTestCase): transaction.atomic(Callable()) -class IgnorePendingDeprecationWarningsMixin(object): - - def setUp(self): - super(IgnorePendingDeprecationWarningsMixin, self).setUp() - self.catch_warnings = warnings.catch_warnings() - self.catch_warnings.__enter__() - warnings.filterwarnings("ignore", category=PendingDeprecationWarning) - - def tearDown(self): - self.catch_warnings.__exit__(*sys.exc_info()) - super(IgnorePendingDeprecationWarningsMixin, self).tearDown() - - class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def create_a_reporter_then_fail(self, first, last): diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index fb3f257dab..5339b4a8ea 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -4,11 +4,9 @@ from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, D IntegrityError) from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError from django.test import TransactionTestCase, skipUnlessDBFeature -from django.test.utils import override_settings +from django.test.utils import override_settings, IgnorePendingDeprecationWarningsMixin from django.utils.unittest import skipIf, skipUnless -from transactions.tests import IgnorePendingDeprecationWarningsMixin - from .models import Mod, M2mA, M2mB, SubMod class ModelInheritanceTests(TransactionTestCase): diff --git a/tests/unmanaged_models/tests.py b/tests/unmanaged_models/tests.py index 64e33bbb47..d7cf961a37 100644 --- a/tests/unmanaged_models/tests.py +++ b/tests/unmanaged_models/tests.py @@ -23,14 +23,14 @@ class SimpleTests(TestCase): # ... and pull it out via the other set. a2 = A02.objects.all()[0] - self.assertTrue(isinstance(a2, A02)) + self.assertIsInstance(a2, A02) self.assertEqual(a2.f_a, "foo") b2 = B02.objects.all()[0] - self.assertTrue(isinstance(b2, B02)) + self.assertIsInstance(b2, B02) self.assertEqual(b2.f_a, "fred") - self.assertTrue(isinstance(b2.fk_a, A02)) + self.assertIsInstance(b2.fk_a, A02) self.assertEqual(b2.fk_a.f_a, "foo") self.assertEqual(list(C02.objects.filter(f_a=None)), []) @@ -38,7 +38,7 @@ class SimpleTests(TestCase): resp = list(C02.objects.filter(mm_a=a.id)) self.assertEqual(len(resp), 1) - self.assertTrue(isinstance(resp[0], C02)) + self.assertIsInstance(resp[0], C02) self.assertEqual(resp[0].f_a, 'barney') diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 24f96fac7c..f54c796d30 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -246,7 +246,7 @@ class ResolverTests(unittest.TestCase): self.assertEqual(len(e.args[0]['tried']), len(url_types_names), 'Wrong number of tried URLs returned. Expected %s, got %s.' % (len(url_types_names), len(e.args[0]['tried']))) for tried, expected in zip(e.args[0]['tried'], url_types_names): for t, e in zip(tried, expected): - self.assertTrue(isinstance(t, e['type']), str('%s is not an instance of %s') % (t, e['type'])) + self.assertIsInstance(t, e['type']), str('%s is not an instance of %s') % (t, e['type']) if 'name' in e: if not e['name']: self.assertTrue(t.name is None, 'Expected no URL name but found %s.' % t.name) @@ -278,11 +278,11 @@ class ReverseShortcutTests(TestCase): return "/hi-there/" res = redirect(FakeObj()) - self.assertTrue(isinstance(res, HttpResponseRedirect)) + self.assertIsInstance(res, HttpResponseRedirect) self.assertEqual(res.url, '/hi-there/') res = redirect(FakeObj(), permanent=True) - self.assertTrue(isinstance(res, HttpResponsePermanentRedirect)) + self.assertIsInstance(res, HttpResponsePermanentRedirect) self.assertEqual(res.url, '/hi-there/') def test_redirect_to_view_name(self): @@ -516,7 +516,7 @@ class RequestURLconfTests(TestCase): b''.join(self.client.get('/second_test/')) class ErrorHandlerResolutionTests(TestCase): - """Tests for handler404 and handler500""" + """Tests for handler400, handler404 and handler500""" def setUp(self): from django.core.urlresolvers import RegexURLResolver @@ -528,12 +528,14 @@ class ErrorHandlerResolutionTests(TestCase): def test_named_handlers(self): from .views import empty_view handler = (empty_view, {}) + self.assertEqual(self.resolver.resolve400(), handler) self.assertEqual(self.resolver.resolve404(), handler) self.assertEqual(self.resolver.resolve500(), handler) def test_callable_handers(self): from .views import empty_view handler = (empty_view, {}) + self.assertEqual(self.callable_resolver.resolve400(), handler) self.assertEqual(self.callable_resolver.resolve404(), handler) self.assertEqual(self.callable_resolver.resolve500(), handler) diff --git a/tests/urlpatterns_reverse/urls_error_handlers.py b/tests/urlpatterns_reverse/urls_error_handlers.py index be4f42afbc..7146fdf43c 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers.py +++ b/tests/urlpatterns_reverse/urls_error_handlers.py @@ -4,5 +4,6 @@ from django.conf.urls import patterns urlpatterns = patterns('') +handler400 = 'urlpatterns_reverse.views.empty_view' handler404 = 'urlpatterns_reverse.views.empty_view' handler500 = 'urlpatterns_reverse.views.empty_view' diff --git a/tests/urlpatterns_reverse/urls_error_handlers_callables.py b/tests/urlpatterns_reverse/urls_error_handlers_callables.py index fe2d3137e9..befeccaf45 100644 --- a/tests/urlpatterns_reverse/urls_error_handlers_callables.py +++ b/tests/urlpatterns_reverse/urls_error_handlers_callables.py @@ -9,5 +9,6 @@ from .views import empty_view urlpatterns = patterns('') +handler400 = empty_view handler404 = empty_view handler500 = empty_view diff --git a/tests/utils_tests/test_baseconv.py b/tests/utils_tests/test_baseconv.py index cc413b4e8e..d69a3a6412 100644 --- a/tests/utils_tests/test_baseconv.py +++ b/tests/utils_tests/test_baseconv.py @@ -1,4 +1,4 @@ -from unittest import TestCase +from django.utils.unittest import TestCase from django.utils.baseconv import base2, base16, base36, base56, base62, base64, BaseConverter from django.utils.six.moves import xrange @@ -39,4 +39,4 @@ class TestBaseConv(TestCase): def test_exception(self): self.assertRaises(ValueError, BaseConverter, 'abc', sign='a') - self.assertTrue(isinstance(BaseConverter('abc', sign='d'), BaseConverter)) + self.assertIsInstance(BaseConverter('abc', sign='d'), BaseConverter) diff --git a/tests/utils_tests/test_html.py b/tests/utils_tests/test_html.py index 090cc32d1c..b973f1c64f 100644 --- a/tests/utils_tests/test_html.py +++ b/tests/utils_tests/test_html.py @@ -5,6 +5,7 @@ import os from django.utils import html from django.utils._os import upath +from django.utils.encoding import force_text from django.utils.unittest import TestCase @@ -63,10 +64,15 @@ class TestUtilsHtml(TestCase): def test_strip_tags(self): f = html.strip_tags items = ( + ('<p>See: 'é is an apostrophe followed by e acute</p>', + 'See: 'é is an apostrophe followed by e acute'), ('<adf>a', 'a'), ('</adf>a', 'a'), ('<asdf><asdf>e', 'e'), - ('<f', '<f'), + ('hi, <f x', 'hi, <f x'), + ('234<235, right?', '234<235, right?'), + ('a4<a5 right?', 'a4<a5 right?'), + ('b7>b2!', 'b7>b2!'), ('</fe', '</fe'), ('<x>b<y>', 'b'), ('a<p onclick="alert(\'<test>\')">b</p>c', 'abc'), @@ -81,8 +87,9 @@ class TestUtilsHtml(TestCase): for filename in ('strip_tags1.html', 'strip_tags2.txt'): path = os.path.join(os.path.dirname(upath(__file__)), 'files', filename) with open(path, 'r') as fp: + content = force_text(fp.read()) start = datetime.now() - stripped = html.strip_tags(fp.read()) + stripped = html.strip_tags(content) elapsed = datetime.now() - start self.assertEqual(elapsed.seconds, 0) self.assertIn("Please try again.", stripped) diff --git a/tests/utils_tests/test_safestring.py b/tests/utils_tests/test_safestring.py new file mode 100644 index 0000000000..a6f0c0a01c --- /dev/null +++ b/tests/utils_tests/test_safestring.py @@ -0,0 +1,53 @@ +from __future__ import absolute_import, unicode_literals + + +from django.template import Template, Context +from django.test import TestCase +from django.utils.encoding import force_text, force_bytes +from django.utils.functional import lazy, Promise +from django.utils.html import escape, conditional_escape +from django.utils.safestring import mark_safe, mark_for_escaping +from django.utils import six +from django.utils import translation + +lazystr = lazy(force_text, six.text_type) +lazybytes = lazy(force_bytes, bytes) + + +class SafeStringTest(TestCase): + def assertRenderEqual(self, tpl, expected, **context): + context = Context(context) + tpl = Template(tpl) + self.assertEqual(tpl.render(context), expected) + + def test_mark_safe(self): + s = mark_safe('a&b') + + self.assertRenderEqual('{{ s }}', 'a&b', s=s) + self.assertRenderEqual('{{ s|force_escape }}', 'a&b', s=s) + + def test_mark_safe_lazy(self): + s = lazystr('a&b') + b = lazybytes(b'a&b') + + self.assertIsInstance(mark_safe(s), Promise) + self.assertIsInstance(mark_safe(b), Promise) + self.assertRenderEqual('{{ s }}', 'a&b', s=mark_safe(s)) + + def test_mark_for_escaping(self): + s = mark_for_escaping('a&b') + self.assertRenderEqual('{{ s }}', 'a&b', s=s) + self.assertRenderEqual('{{ s }}', 'a&b', s=mark_for_escaping(s)) + + def test_mark_for_escaping_lazy(self): + s = lazystr('a&b') + b = lazybytes(b'a&b') + + self.assertIsInstance(mark_for_escaping(s), Promise) + self.assertIsInstance(mark_for_escaping(b), Promise) + self.assertRenderEqual('{% autoescape off %}{{ s }}{% endautoescape %}', 'a&b', s=mark_for_escaping(s)) + + def test_regression_20296(self): + s = mark_safe(translation.ugettext_lazy("username")) + with translation.override('fr'): + self.assertRenderEqual('{{ s }}', "nom d'utilisateur", s=s) diff --git a/tests/utils_tests/test_simplelazyobject.py b/tests/utils_tests/test_simplelazyobject.py index f925e01eb6..4c01bd3adf 100644 --- a/tests/utils_tests/test_simplelazyobject.py +++ b/tests/utils_tests/test_simplelazyobject.py @@ -68,7 +68,7 @@ class TestUtilsSimpleLazyObject(TestCase): # Second, for an evaluated SimpleLazyObject name = x.name # evaluate - self.assertTrue(isinstance(x._wrapped, _ComplexObject)) + self.assertIsInstance(x._wrapped, _ComplexObject) # __repr__ contains __repr__ of wrapped object self.assertEqual("<SimpleLazyObject: %r>" % x._wrapped, repr(x)) @@ -161,3 +161,25 @@ class TestUtilsSimpleLazyObject(TestCase): self.assertNotEqual(lazy1, lazy3) self.assertTrue(lazy1 != lazy3) self.assertFalse(lazy1 != lazy2) + + def test_pickle_py2_regression(self): + from django.contrib.auth.models import User + + # See ticket #20212 + user = User.objects.create_user('johndoe', 'john@example.com', 'pass') + x = SimpleLazyObject(lambda: user) + + # This would fail with "TypeError: can't pickle instancemethod objects", + # only on Python 2.X. + pickled = pickle.dumps(x) + + # Try the variant protocol levels. + pickled = pickle.dumps(x, 0) + pickled = pickle.dumps(x, 1) + pickled = pickle.dumps(x, 2) + + if not six.PY3: + import cPickle + + # This would fail with "TypeError: expected string or Unicode object, NoneType found". + pickled = cPickle.dumps(x) diff --git a/tests/utils_tests/test_timesince.py b/tests/utils_tests/test_timesince.py index 5e641a42c4..cdb95e6877 100644 --- a/tests/utils_tests/test_timesince.py +++ b/tests/utils_tests/test_timesince.py @@ -21,32 +21,33 @@ class TimesinceTests(unittest.TestCase): def test_equal_datetimes(self): """ equal datetimes. """ - self.assertEqual(timesince(self.t, self.t), '0 minutes') + # NOTE: \xa0 avoids wrapping between value and unit + self.assertEqual(timesince(self.t, self.t), '0\xa0minutes') def test_ignore_microseconds_and_seconds(self): """ Microseconds and seconds are ignored. """ self.assertEqual(timesince(self.t, self.t+self.onemicrosecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t+self.onesecond), - '0 minutes') + '0\xa0minutes') def test_other_units(self): """ Test other units. """ self.assertEqual(timesince(self.t, self.t+self.oneminute), - '1 minute') - self.assertEqual(timesince(self.t, self.t+self.onehour), '1 hour') - self.assertEqual(timesince(self.t, self.t+self.oneday), '1 day') - self.assertEqual(timesince(self.t, self.t+self.oneweek), '1 week') + '1\xa0minute') + self.assertEqual(timesince(self.t, self.t+self.onehour), '1\xa0hour') + self.assertEqual(timesince(self.t, self.t+self.oneday), '1\xa0day') + self.assertEqual(timesince(self.t, self.t+self.oneweek), '1\xa0week') self.assertEqual(timesince(self.t, self.t+self.onemonth), - '1 month') - self.assertEqual(timesince(self.t, self.t+self.oneyear), '1 year') + '1\xa0month') + self.assertEqual(timesince(self.t, self.t+self.oneyear), '1\xa0year') def test_multiple_units(self): """ Test multiple units. """ self.assertEqual(timesince(self.t, - self.t+2*self.oneday+6*self.onehour), '2 days, 6 hours') + self.t+2*self.oneday+6*self.onehour), '2\xa0days, 6\xa0hours') self.assertEqual(timesince(self.t, - self.t+2*self.oneweek+2*self.oneday), '2 weeks, 2 days') + self.t+2*self.oneweek+2*self.oneday), '2\xa0weeks, 2\xa0days') def test_display_first_unit(self): """ @@ -55,10 +56,10 @@ class TimesinceTests(unittest.TestCase): """ self.assertEqual(timesince(self.t, self.t+2*self.oneweek+3*self.onehour+4*self.oneminute), - '2 weeks') + '2\xa0weeks') self.assertEqual(timesince(self.t, - self.t+4*self.oneday+5*self.oneminute), '4 days') + self.t+4*self.oneday+5*self.oneminute), '4\xa0days') def test_display_second_before_first(self): """ @@ -66,30 +67,30 @@ class TimesinceTests(unittest.TestCase): get 0 minutes. """ self.assertEqual(timesince(self.t, self.t-self.onemicrosecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onesecond), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneminute), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onehour), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneday), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneweek), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.onemonth), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-self.oneyear), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-2*self.oneday-6*self.onehour), '0 minutes') + self.t-2*self.oneday-6*self.onehour), '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-2*self.oneweek-2*self.oneday), '0 minutes') + self.t-2*self.oneweek-2*self.oneday), '0\xa0minutes') self.assertEqual(timesince(self.t, self.t-2*self.oneweek-3*self.onehour-4*self.oneminute), - '0 minutes') + '0\xa0minutes') self.assertEqual(timesince(self.t, - self.t-4*self.oneday-5*self.oneminute), '0 minutes') + self.t-4*self.oneday-5*self.oneminute), '0\xa0minutes') def test_different_timezones(self): """ When using two different timezones. """ @@ -97,28 +98,28 @@ class TimesinceTests(unittest.TestCase): now_tz = datetime.datetime.now(LocalTimezone(now)) now_tz_i = datetime.datetime.now(FixedOffset((3 * 60) + 15)) - self.assertEqual(timesince(now), '0 minutes') - self.assertEqual(timesince(now_tz), '0 minutes') - self.assertEqual(timeuntil(now_tz, now_tz_i), '0 minutes') + self.assertEqual(timesince(now), '0\xa0minutes') + self.assertEqual(timesince(now_tz), '0\xa0minutes') + self.assertEqual(timeuntil(now_tz, now_tz_i), '0\xa0minutes') def test_date_objects(self): """ Both timesince and timeuntil should work on date objects (#17937). """ today = datetime.date.today() - self.assertEqual(timesince(today + self.oneday), '0 minutes') - self.assertEqual(timeuntil(today - self.oneday), '0 minutes') + self.assertEqual(timesince(today + self.oneday), '0\xa0minutes') + self.assertEqual(timeuntil(today - self.oneday), '0\xa0minutes') def test_both_date_objects(self): """ Timesince should work with both date objects (#9672) """ today = datetime.date.today() - self.assertEqual(timeuntil(today + self.oneday, today), '1 day') - self.assertEqual(timeuntil(today - self.oneday, today), '0 minutes') - self.assertEqual(timeuntil(today + self.oneweek, today), '1 week') + self.assertEqual(timeuntil(today + self.oneday, today), '1\xa0day') + self.assertEqual(timeuntil(today - self.oneday, today), '0\xa0minutes') + self.assertEqual(timeuntil(today + self.oneweek, today), '1\xa0week') def test_naive_datetime_with_tzinfo_attribute(self): class naive(datetime.tzinfo): def utcoffset(self, dt): return None future = datetime.datetime(2080, 1, 1, tzinfo=naive()) - self.assertEqual(timesince(future), '0 minutes') + self.assertEqual(timesince(future), '0\xa0minutes') past = datetime.datetime(1980, 1, 1, tzinfo=naive()) - self.assertEqual(timeuntil(past), '0 minutes') + self.assertEqual(timeuntil(past), '0\xa0minutes') diff --git a/tests/validation/models.py b/tests/validation/models.py index db083290fb..e95a1e0744 100644 --- a/tests/validation/models.py +++ b/tests/validation/models.py @@ -95,7 +95,7 @@ class GenericIPAddressTestModel(models.Model): blank=True, null=True) class GenericIPAddrUnpackUniqueTest(models.Model): - generic_v4unpack_ip = models.GenericIPAddressField(blank=True, unique=True, unpack_ipv4=True) + generic_v4unpack_ip = models.GenericIPAddressField(null=True, blank=True, unique=True, unpack_ipv4=True) # A model can't have multiple AutoFields diff --git a/tests/validation/tests.py b/tests/validation/tests.py index b571e0c298..9ddf796c2b 100644 --- a/tests/validation/tests.py +++ b/tests/validation/tests.py @@ -109,9 +109,9 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_generic_ip_passes(self): giptm = GenericIPAddressTestModel(generic_ip="1.2.3.4") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) giptm = GenericIPAddressTestModel(generic_ip="2001::2") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_generic_ip_raises_error(self): giptm = GenericIPAddressTestModel(generic_ip="294.4.2.1") @@ -121,7 +121,7 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_v4_ip_passes(self): giptm = GenericIPAddressTestModel(v4_ip="1.2.3.4") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_v4_ip_raises_error(self): giptm = GenericIPAddressTestModel(v4_ip="294.4.2.1") @@ -131,7 +131,7 @@ class GenericIPAddressFieldTests(ValidationTestCase): def test_correct_v6_ip_passes(self): giptm = GenericIPAddressTestModel(v6_ip="2001::2") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) def test_invalid_v6_ip_raises_error(self): giptm = GenericIPAddressTestModel(v6_ip="1.2.3.4") @@ -151,10 +151,16 @@ class GenericIPAddressFieldTests(ValidationTestCase): giptm = GenericIPAddressTestModel(generic_ip="::ffff:10.10.10.10") giptm.save() giptm = GenericIPAddressTestModel(generic_ip="10.10.10.10") - self.assertEqual(None, giptm.full_clean()) + self.assertIsNone(giptm.full_clean()) # These two are the same, because we are doing IPv4 unpacking giptm = GenericIPAddrUnpackUniqueTest(generic_v4unpack_ip="::ffff:18.52.18.52") giptm.save() giptm = GenericIPAddrUnpackUniqueTest(generic_v4unpack_ip="18.52.18.52") self.assertFailsValidation(giptm.full_clean, ['generic_v4unpack_ip',]) + + def test_empty_generic_ip_passes(self): + giptm = GenericIPAddressTestModel(generic_ip="") + self.assertIsNone(giptm.full_clean()) + giptm = GenericIPAddressTestModel(generic_ip=None) + self.assertIsNone(giptm.full_clean()) diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py index b44cd88abe..f686eee0e0 100644 --- a/tests/view_tests/tests/test_debug.py +++ b/tests/view_tests/tests/test_debug.py @@ -5,7 +5,9 @@ from __future__ import absolute_import, unicode_literals import inspect import os +import shutil import sys +from tempfile import NamedTemporaryFile, mkdtemp, mkstemp from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile @@ -13,13 +15,14 @@ from django.core.urlresolvers import reverse from django.test import TestCase, RequestFactory from django.test.utils import (override_settings, setup_test_template_loader, restore_template_loaders) -from django.utils.encoding import force_text +from django.utils.encoding import force_text, force_bytes from django.views.debug import ExceptionReporter from .. import BrokenException, except_args from ..views import (sensitive_view, non_sensitive_view, paranoid_view, custom_exception_reporter_filter_view, sensitive_method_view, sensitive_args_function_caller, sensitive_kwargs_function_caller) +from django.utils.unittest import skipIf @override_settings(DEBUG=True, TEMPLATE_DEBUG=True) @@ -77,9 +80,38 @@ class DebugViewTests(TestCase): raising_loc) def test_template_loader_postmortem(self): - response = self.client.get(reverse('raises_template_does_not_exist')) - template_path = os.path.join('templates', 'i_dont_exist.html') - self.assertContains(response, template_path, status_code=500) + """Tests for not existing file""" + template_name = "notfound.html" + with NamedTemporaryFile(prefix=template_name) as tempfile: + tempdir = os.path.dirname(tempfile.name) + template_path = os.path.join(tempdir, template_name) + with override_settings(TEMPLATE_DIRS=(tempdir,)): + response = self.client.get(reverse('raises_template_does_not_exist', kwargs={"path": template_name})) + self.assertContains(response, "%s (File does not exist)" % template_path, status_code=500, count=1) + + @skipIf(sys.platform == "win32", "Python on Windows doesn't have working os.chmod() and os.access().") + def test_template_loader_postmortem_notreadable(self): + """Tests for not readable file""" + with NamedTemporaryFile() as tempfile: + template_name = tempfile.name + tempdir = os.path.dirname(tempfile.name) + template_path = os.path.join(tempdir, template_name) + os.chmod(template_path, 0o0222) + with override_settings(TEMPLATE_DIRS=(tempdir,)): + response = self.client.get(reverse('raises_template_does_not_exist', kwargs={"path": template_name})) + self.assertContains(response, "%s (File is not readable)" % template_path, status_code=500, count=1) + + def test_template_loader_postmortem_notafile(self): + """Tests for not being a file""" + try: + template_path = mkdtemp() + template_name = os.path.basename(template_path) + tempdir = os.path.dirname(template_path) + with override_settings(TEMPLATE_DIRS=(tempdir,)): + response = self.client.get(reverse('raises_template_does_not_exist', kwargs={"path": template_name})) + self.assertContains(response, "%s (Not a file)" % template_path, status_code=500, count=1) + finally: + shutil.rmtree(template_path) class ExceptionReporterTests(TestCase): @@ -122,13 +154,31 @@ class ExceptionReporterTests(TestCase): self.assertIn('<h2>Request information</h2>', html) self.assertIn('<p>Request data not supplied</p>', html) + def test_eol_support(self): + """Test that the ExceptionReporter supports Unix, Windows and Macintosh EOL markers""" + LINES = list('print %d' % i for i in range(1, 6)) + reporter = ExceptionReporter(None, None, None, None) + + for newline in ['\n', '\r\n', '\r']: + fd, filename = mkstemp(text=False) + os.write(fd, force_bytes(newline.join(LINES)+newline)) + os.close(fd) + + try: + self.assertEqual( + reporter._get_lines_from_file(filename, 3, 2), + (1, LINES[1:3], LINES[3], LINES[4:]) + ) + finally: + os.unlink(filename) + def test_no_exception(self): "An exception report can be generated for just a request" request = self.rf.get('/test_view/') reporter = ExceptionReporter(request, None, None, None) html = reporter.get_traceback_html() self.assertIn('<h1>Report at /test_view/</h1>', html) - self.assertIn('<pre class="exception_value">No exception supplied</pre>', html) + self.assertIn('<pre class="exception_value">No exception message supplied</pre>', html) self.assertIn('<th>Request Method:</th>', html) self.assertIn('<th>Request URL:</th>', html) self.assertNotIn('<th>Exception Type:</th>', html) diff --git a/tests/view_tests/tests/test_static.py b/tests/view_tests/tests/test_static.py index bdd9fbfc0b..6104ad063e 100644 --- a/tests/view_tests/tests/test_static.py +++ b/tests/view_tests/tests/test_static.py @@ -60,7 +60,7 @@ class StaticTests(TestCase): # This is 24h before max Unix time. Remember to fix Django and # update this test well before 2038 :) ) - self.assertTrue(isinstance(response, HttpResponseNotModified)) + self.assertIsInstance(response, HttpResponseNotModified) def test_invalid_if_modified_since(self): """Handle bogus If-Modified-Since values gracefully diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 52e2eb474e..d792e47ddf 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -66,5 +66,5 @@ urlpatterns = patterns('', urlpatterns += patterns('view_tests.views', url(r'view_exception/(?P<n>\d+)/$', 'view_exception', name='view_exception'), url(r'template_exception/(?P<n>\d+)/$', 'template_exception', name='template_exception'), - url(r'^raises_template_does_not_exist/$', 'raises_template_does_not_exist', name='raises_template_does_not_exist'), + url(r'^raises_template_does_not_exist/(?P<path>.+)$', 'raises_template_does_not_exist', name='raises_template_does_not_exist'), ) diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index 50ad98ac2d..1cfafa4333 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -112,11 +112,11 @@ def render_view_with_current_app_conflict(request): 'bar': 'BAR', }, current_app="foobar_app", context_instance=RequestContext(request)) -def raises_template_does_not_exist(request): +def raises_template_does_not_exist(request, path='i_dont_exist.html'): # We need to inspect the HTML generated by the fancy 500 debug view but # the test client ignores it, so we send it explicitly. try: - return render_to_response('i_dont_exist.html') + return render_to_response(path) except TemplateDoesNotExist: return technical_500_response(request, *sys.exc_info())