diff --git a/AUTHORS b/AUTHORS index 4921f7c3ad85..3656e92a42d0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,6 +18,11 @@ The PRIMARY AUTHORS are (and/or have been): * Karen Tracey * Jannis Leidel * James Tauber + * Alex Gaynor + * Andrew Godwin + * Carl Meyer + * Ramiro Morales + * Chris Beaven More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -29,7 +34,6 @@ answer newbie questions, and generally made Django that much better: Gisle Aas ajs alang@bright-green.com - Alcides Fonseca Andi Albrecht Marty Alchin Ahmad Alhashemi @@ -39,7 +43,6 @@ answer newbie questions, and generally made Django that much better: AgarFu Dagur Páll Ammendrup Collin Anderson - Nicolas Lara Jeff Anderson Marian Andre Andreas @@ -67,7 +70,6 @@ answer newbie questions, and generally made Django that much better: Ned Batchelder batiste@dosimple.ch Batman - Chris Beaven Brian Beck Shannon -jj Behrens Esdras Beleza @@ -84,20 +86,21 @@ answer newbie questions, and generally made Django that much better: Matías Bordese Sean Brant Andrew Brehaut + David Brenneman brut.alll@gmail.com + bthomas btoll@bestweb.net Jonathan Buchanan Keith Bussell + C8E Chris Cahoon Juan Manuel Caicedo Trevor Caira Brett Cannon Ricardo Javier Cárdenes Medina Jeremy Carbaugh - Carl Meyer Graham Carlyle Antonio Cavedoni - C8E cedric@terramater.net Chris Chamberlin Amit Chakradeo @@ -110,6 +113,7 @@ answer newbie questions, and generally made Django that much better: Michal Chruszcz Can Burak Çilingir Ian Clelland + Travis Cline Russell Cloran colin@owlfish.com crankycoder@gmail.com @@ -123,7 +127,7 @@ answer newbie questions, and generally made Django that much better: John D'Agostino dackze+django@gmail.com Mihai Damian - David Danier + David Danier Dirk Datzert Jonathan Daugherty (cygnus) dave@thebarproject.com @@ -137,6 +141,7 @@ answer newbie questions, and generally made Django that much better: Rajesh Dhawan Sander Dijkhuis Jordan Dimov + Nebojša Dorđević dne@mayonnaise.net dready Maximillian Dornseif @@ -167,7 +172,6 @@ answer newbie questions, and generally made Django that much better: Liang Feng Bill Fenner Stefane Fermgier - Afonso Fernández Nogueira J. Pablo Fernandez Maciej Fijalkowski Ben Firshman @@ -175,11 +179,11 @@ answer newbie questions, and generally made Django that much better: Eric Floehr Eric Florenzano Vincent Foley + Alcides Fonseca Rudolph Froger Jorge Gajon gandalf@owca.info Marc Garcia - Alex Gaynor Andy Gayton Idan Gazit geber@datacollect.com @@ -195,6 +199,7 @@ answer newbie questions, and generally made Django that much better: David Gouldin pradeep.gowda@gmail.com Collin Grady + Gabriel Grant Simon Greenhill Owen Griffiths Espen Grindhaug @@ -231,6 +236,7 @@ answer newbie questions, and generally made Django that much better: Ibon Tom Insam Baurzhan Ismagulov + Stephan Jaekel james_027@yahoo.com jcrasta@gmail.com jdetaeye @@ -244,9 +250,11 @@ answer newbie questions, and generally made Django that much better: Bahadır Kandemir Karderio Nagy Károly + George Karpenkov Erik Karulf Ben Dean Kawamura Ian G. Kelly + Niall Kelly Ryan Kelly Thomas Kerpe Wiley Kestner @@ -275,11 +283,11 @@ answer newbie questions, and generally made Django that much better: kurtiss@meetro.com Denis Kuzmichyov Panos Laganakos - Lakin Wecker Nick Lane Stuart Langridge Paul Lanier David Larlet + Nicolas Lara Nicola Larosa Finn Gruwier Larsen Lau Bech Lauritzen @@ -298,9 +306,9 @@ answer newbie questions, and generally made Django that much better: limodou Philip Lindborg Simon Litchfield - Daniel Lindsley + Daniel Lindsley Trey Long - msaelices + Laurent Luce Martin Mahner Matt McClanahan Stanislaus Madueke @@ -313,20 +321,20 @@ answer newbie questions, and generally made Django that much better: Petr Marhoun Petar Marić Nuno Mariz - Marijn Vriens mark@junklight.com Orestis Markou Takashi Matsuo Yasushi Masuda mattycakes@gmail.com + Glenn Maynard Jason McBrayer Kevin McConnell mccutchen@gmail.com + michael.mcewan@gmail.com Paul McLanahan Tobias McNulty Zain Memon Christian Metts - michael.mcewan@gmail.com michal@plovarna.cz Slawek Mikula mitakummaa@gmail.com @@ -334,14 +342,14 @@ answer newbie questions, and generally made Django that much better: Andreas Mock Reza Mohammadi Aljosa Mohorovic - Ramiro Morales Eric Moritz + msaelices + Gregor Müllegger Robin Munn James Murty msundstr Robert Myers Alexander Myodov - Nebojša Dorđević Doug Napoleone Gopal Narayanan Fraser Nevett @@ -369,7 +377,6 @@ answer newbie questions, and generally made Django that much better: phil.h.smith@gmail.com Gustavo Picon Michael Placentra II - Luke Plant plisk Daniel Poelzleithner polpak@yahoo.com @@ -400,7 +407,6 @@ answer newbie questions, and generally made Django that much better: Henrique Romano Armin Ronacher Daniel Roseman - Brian Rosner Rozza Oliver Rutherfurd ryankanno @@ -479,6 +485,7 @@ answer newbie questions, and generally made Django that much better: George Vilches Vlado Zachary Voase + Marijn Vriens Milton Waddams Chris Wagner Rick Wagner @@ -487,6 +494,7 @@ answer newbie questions, and generally made Django that much better: Filip Wasilewski Dan Watson Joel Watts + Lakin Wecker Chris Wesseling James Wheare Mike Wiacek @@ -508,8 +516,6 @@ answer newbie questions, and generally made Django that much better: Gasper Zejn Jarek Zgoda Cheng Zhang - Glenn Maynard - bthomas A big THANK YOU goes to: diff --git a/MANIFEST.in b/MANIFEST.in index 3fbad67b1d2c..5175b0870f45 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,12 +3,12 @@ include AUTHORS include INSTALL include LICENSE include MANIFEST.in -include django/utils/simplejson/LICENSE.txt include django/contrib/gis/gdal/LICENSE include django/contrib/gis/geos/LICENSE +include django/dispatch/license.txt +include django/utils/simplejson/LICENSE.txt recursive-include docs * recursive-include scripts * -recursive-include examples * recursive-include extras * recursive-include tests * recursive-include django/conf/locale * @@ -20,8 +20,12 @@ recursive-include django/contrib/auth/tests/templates * recursive-include django/contrib/comments/templates * recursive-include django/contrib/databrowse/templates * recursive-include django/contrib/formtools/templates * +recursive-include django/contrib/flatpages/fixtures * +recursive-include django/contrib/flatpages/tests/templates * recursive-include django/contrib/gis/templates * recursive-include django/contrib/gis/tests/data * +recursive-include django/contrib/gis/tests/distapp/fixtures * recursive-include django/contrib/gis/tests/geoapp/fixtures * recursive-include django/contrib/gis/tests/geogapp/fixtures * +recursive-include django/contrib/gis/tests/relatedapp/fixtures * recursive-include django/contrib/sitemaps/templates * diff --git a/README b/README index e748d9b7d060..c1cd37e0a094 100644 --- a/README +++ b/README @@ -35,3 +35,9 @@ To contribute to Django: * Check out http://www.djangoproject.com/community/ for information about getting involved. + +To run Django's test suite: + + * Follow the instructions in the "Unit tests" section of + docs/internals/contributing.txt, published online at + http://docs.djangoproject.com/en/dev/internals/contributing/#running-the-unit-tests diff --git a/django/__init__.py b/django/__init__.py index cdaf1ec40eec..835490b308bb 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1,4 +1,4 @@ -VERSION = (1, 2, 1, 'final', 0) +VERSION = (1, 2, 7, 'final', 0) def get_version(): version = '%s.%s' % (VERSION[0], VERSION[1]) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index cdff2ff13f02..b23b53cce16f 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -54,7 +54,7 @@ ('en', gettext_noop('English')), ('en-gb', gettext_noop('British English')), ('es', gettext_noop('Spanish')), - ('es-ar', gettext_noop('Argentinean Spanish')), + ('es-ar', gettext_noop('Argentinian Spanish')), ('et', gettext_noop('Estonian')), ('eu', gettext_noop('Basque')), ('fa', gettext_noop('Persian')), @@ -78,6 +78,7 @@ ('lt', gettext_noop('Lithuanian')), ('lv', gettext_noop('Latvian')), ('mk', gettext_noop('Macedonian')), + ('ml', gettext_noop('Malayalam')), ('mn', gettext_noop('Mongolian')), ('nl', gettext_noop('Dutch')), ('no', gettext_noop('Norwegian')), @@ -257,7 +258,7 @@ # Default file storage mechanism that holds media. DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage' -# Absolute path to the directory that holds media. +# Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = '' @@ -291,34 +292,34 @@ FORMAT_MODULE_PATH = None # Default formatting for date objects. See all available format strings here: -# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#now +# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date DATE_FORMAT = 'N j, Y' # Default formatting for datetime objects. See all available format strings here: -# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#now +# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date DATETIME_FORMAT = 'N j, Y, P' # Default formatting for time objects. See all available format strings here: -# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#now +# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date TIME_FORMAT = 'P' # Default formatting for date objects when only the year and month are relevant. # See all available format strings here: -# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#now +# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date YEAR_MONTH_FORMAT = 'F Y' # Default formatting for date objects when only the month and day are relevant. # See all available format strings here: -# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#now +# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date MONTH_DAY_FORMAT = 'F j' # Default short formatting for date objects. See all available format strings here: -# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#now +# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date SHORT_DATE_FORMAT = 'm/d/Y' # Default short formatting for datetime objects. # See all available format strings here: -# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#now +# http://docs.djangoproject.com/en/dev/ref/templates/builtins/#date SHORT_DATETIME_FORMAT = 'm/d/Y P' # Default formats to be used when parsing dates from input boxes, in order @@ -369,8 +370,8 @@ # Boolean that sets whether to add thousand separator when formatting numbers USE_THOUSAND_SEPARATOR = False -# Number of digits that will be togheter, when spliting them by THOUSAND_SEPARATOR -# 0 means no grouping, 3 means splitting by thousands... +# Number of digits that will be together, when spliting them by +# THOUSAND_SEPARATOR. 0 means no grouping, 3 means splitting by thousands... NUMBER_GROUPING = 0 # Thousand separator symbol @@ -389,6 +390,8 @@ DEFAULT_TABLESPACE = '' DEFAULT_INDEX_TABLESPACE = '' +USE_X_FORWARDED_HOST = False + ############## # MIDDLEWARE # ############## diff --git a/django/conf/locale/ar/LC_MESSAGES/django.mo b/django/conf/locale/ar/LC_MESSAGES/django.mo index 77bc44da15eb..38d5a5832ab0 100644 Binary files a/django/conf/locale/ar/LC_MESSAGES/django.mo and b/django/conf/locale/ar/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ar/LC_MESSAGES/django.po b/django/conf/locale/ar/LC_MESSAGES/django.po index 28e2153032ad..fd3f72adc272 100644 --- a/django/conf/locale/ar/LC_MESSAGES/django.po +++ b/django/conf/locale/ar/LC_MESSAGES/django.po @@ -4801,18 +4801,18 @@ msgstr[3] "%(size)d بايت" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f ك.ب" +msgid "%s KB" +msgstr "%s ك.ب" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f م.ب" +msgid "%s MB" +msgstr "%s م.ب" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f ج.ب" +msgid "%s GB" +msgstr "%s ج.ب" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/bg/LC_MESSAGES/django.mo b/django/conf/locale/bg/LC_MESSAGES/django.mo index c1993c661810..3d3839eb021f 100644 Binary files a/django/conf/locale/bg/LC_MESSAGES/django.mo and b/django/conf/locale/bg/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/bg/LC_MESSAGES/django.po b/django/conf/locale/bg/LC_MESSAGES/django.po index eae6c2bcc182..d61eb7fb6abf 100644 --- a/django/conf/locale/bg/LC_MESSAGES/django.po +++ b/django/conf/locale/bg/LC_MESSAGES/django.po @@ -4935,20 +4935,20 @@ msgstr "%(size)d байт" #: django/template/defaultfilters.py:728 #: template/defaultfilters.py:724 #, python-format -msgid "%.1f KB" -msgstr "%.1f КБ" +msgid "%s KB" +msgstr "%s КБ" #: django/template/defaultfilters.py:730 #: template/defaultfilters.py:726 #, python-format -msgid "%.1f MB" -msgstr "%.1f МБ" +msgid "%s MB" +msgstr "%s МБ" #: django/template/defaultfilters.py:731 #: template/defaultfilters.py:727 #, python-format -msgid "%.1f GB" -msgstr "%.1f ГБ" +msgid "%s GB" +msgstr "%s ГБ" #: django/utils/dateformat.py:41 #: utils/dateformat.py:41 diff --git a/django/conf/locale/bg/formats.py b/django/conf/locale/bg/formats.py index 045543a7ac5b..ea5f300e55a8 100644 --- a/django/conf/locale/bg/formats.py +++ b/django/conf/locale/bg/formats.py @@ -14,5 +14,5 @@ # TIME_INPUT_FORMATS = # DATETIME_INPUT_FORMATS = DECIMAL_SEPARATOR = ',' -THOUSAND_SEPARATOR = ' ' +THOUSAND_SEPARATOR = u' ' # Non-breaking space # NUMBER_GROUPING = diff --git a/django/conf/locale/bn/LC_MESSAGES/django.mo b/django/conf/locale/bn/LC_MESSAGES/django.mo index 770a84627526..684f2eb2f5dc 100644 Binary files a/django/conf/locale/bn/LC_MESSAGES/django.mo and b/django/conf/locale/bn/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/bn/LC_MESSAGES/django.po b/django/conf/locale/bn/LC_MESSAGES/django.po index dfdaa3595fef..3a91d2d567f4 100644 --- a/django/conf/locale/bn/LC_MESSAGES/django.po +++ b/django/conf/locale/bn/LC_MESSAGES/django.po @@ -3733,18 +3733,18 @@ msgstr[1] "%(size)d বাইট" #: template/defaultfilters.py:739 #, python-format -msgid "%.1f KB" -msgstr "%.1f কেবি" +msgid "%s KB" +msgstr "%s কেবি" #: template/defaultfilters.py:741 #, python-format -msgid "%.1f MB" -msgstr "%.1f এমবি" +msgid "%s MB" +msgstr "%s এমবি" #: template/defaultfilters.py:742 #, python-format -msgid "%.1f GB" -msgstr "%.1f জিবি" +msgid "%s GB" +msgstr "%s জিবি" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/bs/LC_MESSAGES/django.mo b/django/conf/locale/bs/LC_MESSAGES/django.mo index 8ca72556ce7f..3994e874a6c7 100644 Binary files a/django/conf/locale/bs/LC_MESSAGES/django.mo and b/django/conf/locale/bs/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/bs/LC_MESSAGES/django.po b/django/conf/locale/bs/LC_MESSAGES/django.po index 31b68e6ea054..6c8583658d66 100644 --- a/django/conf/locale/bs/LC_MESSAGES/django.po +++ b/django/conf/locale/bs/LC_MESSAGES/django.po @@ -4831,18 +4831,18 @@ msgstr[2] "%(size)d bajtova" #: .\template\defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: .\template\defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: .\template\defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: .\utils\dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/ca/LC_MESSAGES/django.mo b/django/conf/locale/ca/LC_MESSAGES/django.mo index 457a8280479e..fb4daa4e0e98 100644 Binary files a/django/conf/locale/ca/LC_MESSAGES/django.mo and b/django/conf/locale/ca/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ca/LC_MESSAGES/django.po b/django/conf/locale/ca/LC_MESSAGES/django.po index f8cd8aa6099d..2417f74984fd 100644 --- a/django/conf/locale/ca/LC_MESSAGES/django.po +++ b/django/conf/locale/ca/LC_MESSAGES/django.po @@ -4157,18 +4157,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:800 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:802 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:803 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/cs/LC_MESSAGES/django.mo b/django/conf/locale/cs/LC_MESSAGES/django.mo index 9251a58fbed5..b4f8dae1ce3b 100644 Binary files a/django/conf/locale/cs/LC_MESSAGES/django.mo and b/django/conf/locale/cs/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/cs/LC_MESSAGES/django.po b/django/conf/locale/cs/LC_MESSAGES/django.po index 4984d4251710..2ab303d6d951 100644 --- a/django/conf/locale/cs/LC_MESSAGES/django.po +++ b/django/conf/locale/cs/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-09 14:26+0200\n" -"PO-Revision-Date: 2010-05-09 14:09+0100\n" +"POT-Creation-Date: 2010-08-06 18:35+0200\n" +"PO-Revision-Date: 2010-08-06 18:33+0100\n" "Last-Translator: Vlada Macek \n" "Language-Team: Czech\n" "MIME-Version: 1.0\n" @@ -70,7 +70,7 @@ msgid "Spanish" msgstr "španělsky" #: conf/global_settings.py:57 -msgid "Argentinean Spanish" +msgid "Argentinian Spanish" msgstr "španělsky (Argentina)" #: conf/global_settings.py:58 @@ -122,138 +122,146 @@ msgid "Hungarian" msgstr "maďarsky" #: conf/global_settings.py:70 +msgid "Indonesian" +msgstr "indonésky" + +#: conf/global_settings.py:71 msgid "Icelandic" msgstr "islandsky" -#: conf/global_settings.py:71 +#: conf/global_settings.py:72 msgid "Italian" msgstr "italsky" -#: conf/global_settings.py:72 +#: conf/global_settings.py:73 msgid "Japanese" msgstr "japonsky" -#: conf/global_settings.py:73 +#: conf/global_settings.py:74 msgid "Georgian" msgstr "gruzínsky" -#: conf/global_settings.py:74 +#: conf/global_settings.py:75 msgid "Khmer" msgstr "khmersky" -#: conf/global_settings.py:75 +#: conf/global_settings.py:76 msgid "Kannada" msgstr "kannadsky" -#: conf/global_settings.py:76 +#: conf/global_settings.py:77 msgid "Korean" msgstr "korejsky" -#: conf/global_settings.py:77 +#: conf/global_settings.py:78 msgid "Lithuanian" msgstr "litevsky" -#: conf/global_settings.py:78 +#: conf/global_settings.py:79 msgid "Latvian" msgstr "lotyšsky" -#: conf/global_settings.py:79 +#: conf/global_settings.py:80 msgid "Macedonian" msgstr "makedonsky" -#: conf/global_settings.py:80 +#: conf/global_settings.py:81 +msgid "Malayalam" +msgstr "malajálamsky" + +#: conf/global_settings.py:82 msgid "Mongolian" msgstr "mongolsky" -#: conf/global_settings.py:81 +#: conf/global_settings.py:83 msgid "Dutch" msgstr "holandsky" -#: conf/global_settings.py:82 +#: conf/global_settings.py:84 msgid "Norwegian" msgstr "norsky" -#: conf/global_settings.py:83 +#: conf/global_settings.py:85 msgid "Norwegian Bokmal" msgstr "norsky (Bokmål)" -#: conf/global_settings.py:84 +#: conf/global_settings.py:86 msgid "Norwegian Nynorsk" msgstr "norsky (Nynorsk)" -#: conf/global_settings.py:85 +#: conf/global_settings.py:87 msgid "Polish" msgstr "polsky" -#: conf/global_settings.py:86 +#: conf/global_settings.py:88 msgid "Portuguese" msgstr "portugalsky" -#: conf/global_settings.py:87 +#: conf/global_settings.py:89 msgid "Brazilian Portuguese" msgstr "portugalsky (Brazílie)" -#: conf/global_settings.py:88 +#: conf/global_settings.py:90 msgid "Romanian" msgstr "rumunsky" -#: conf/global_settings.py:89 +#: conf/global_settings.py:91 msgid "Russian" msgstr "rusky" -#: conf/global_settings.py:90 +#: conf/global_settings.py:92 msgid "Slovak" msgstr "slovensky" -#: conf/global_settings.py:91 +#: conf/global_settings.py:93 msgid "Slovenian" msgstr "slovinsky" -#: conf/global_settings.py:92 +#: conf/global_settings.py:94 msgid "Albanian" msgstr "albánsky" -#: conf/global_settings.py:93 +#: conf/global_settings.py:95 msgid "Serbian" msgstr "srbsky" -#: conf/global_settings.py:94 +#: conf/global_settings.py:96 msgid "Serbian Latin" msgstr "srbsky (latinkou)" -#: conf/global_settings.py:95 +#: conf/global_settings.py:97 msgid "Swedish" msgstr "švédsky" -#: conf/global_settings.py:96 +#: conf/global_settings.py:98 msgid "Tamil" msgstr "tamilsky" -#: conf/global_settings.py:97 +#: conf/global_settings.py:99 msgid "Telugu" msgstr "telužsky" -#: conf/global_settings.py:98 +#: conf/global_settings.py:100 msgid "Thai" msgstr "thajsky" -#: conf/global_settings.py:99 +#: conf/global_settings.py:101 msgid "Turkish" msgstr "turecky" -#: conf/global_settings.py:100 +#: conf/global_settings.py:102 msgid "Ukrainian" msgstr "ukrajinsky" -#: conf/global_settings.py:101 +#: conf/global_settings.py:103 msgid "Vietnamese" msgstr "vietnamsky" -#: conf/global_settings.py:102 +#: conf/global_settings.py:104 msgid "Simplified Chinese" msgstr "čínsky (zjednodušeně)" -#: conf/global_settings.py:103 +#: conf/global_settings.py:105 msgid "Traditional Chinese" msgstr "čínsky (tradičně)" @@ -305,15 +313,15 @@ msgstr "Tento měsíc" msgid "This year" msgstr "Tento rok" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "Yes" msgstr "Ano" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "No" msgstr "Ne" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:478 msgid "Unknown" msgstr "Neznámé" @@ -359,7 +367,7 @@ msgid "Changed %s." msgstr "Změněno: %s" #: contrib/admin/options.py:559 contrib/admin/options.py:569 -#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:844 +#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:845 #: forms/models.py:568 msgid "and" msgstr "a" @@ -853,7 +861,7 @@ msgstr "Uložit a přidat další položku" msgid "Save and continue editing" msgstr "Uložit a pokračovat v úpravách" -#: contrib/admin/templates/admin/auth/user/add_form.html:5 +#: contrib/admin/templates/admin/auth/user/add_form.html:6 msgid "" "First, enter a username and password. Then, you'll be able to edit more user " "options." @@ -861,6 +869,10 @@ msgstr "" "Nejdříve vložte uživatelské jméno a heslo. Poté budete moci upravovat více " "uživatelských nastavení." +#: contrib/admin/templates/admin/auth/user/add_form.html:8 +msgid "Enter a username and password." +msgstr "Vložte uživatelské jméno a heslo." + #: contrib/admin/templates/admin/auth/user/change_password.html:28 #, python-format msgid "Enter a new password for the user %(username)s." @@ -1418,8 +1430,8 @@ msgstr "zpráva" msgid "Logged out" msgstr "Odhlášeno" -#: contrib/auth/management/commands/createsuperuser.py:23 -#: core/validators.py:120 forms/fields.py:428 +#: contrib/auth/management/commands/createsuperuser.py:24 +#: core/validators.py:120 forms/fields.py:427 msgid "Enter a valid e-mail address." msgstr "Vložte platnou e-mailovou adresu." @@ -1491,7 +1503,7 @@ msgid "Email address" msgstr "E-mailová adresa" #: contrib/comments/forms.py:95 contrib/flatpages/admin.py:8 -#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1101 +#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1109 msgid "URL" msgstr "URL" @@ -1541,7 +1553,7 @@ msgstr "komentář" msgid "date/time submitted" msgstr "datum a čas byly zaslané" -#: contrib/comments/models.py:60 db/models/fields/__init__.py:896 +#: contrib/comments/models.py:60 db/models/fields/__init__.py:904 msgid "IP address" msgstr "Adresa IP" @@ -4473,22 +4485,22 @@ msgstr "weby" msgid "Enter a valid value." msgstr "Vložte platnou hodnotu." -#: core/validators.py:87 forms/fields.py:529 +#: core/validators.py:87 forms/fields.py:528 msgid "Enter a valid URL." msgstr "Vložte platnou adresu URL." -#: core/validators.py:89 forms/fields.py:530 +#: core/validators.py:89 forms/fields.py:529 msgid "This URL appears to be a broken link." msgstr "Tato adresa URL je zřejmě neplatný odkaz." -#: core/validators.py:123 forms/fields.py:873 +#: core/validators.py:123 forms/fields.py:877 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Vložte platný identifikátor složený pouze z písmen, čísel, podtržítek a " "pomlček." -#: core/validators.py:126 forms/fields.py:866 +#: core/validators.py:126 forms/fields.py:870 msgid "Enter a valid IPv4 address." msgstr "Vložte platnou adresu typu IPv4." @@ -4501,12 +4513,12 @@ msgstr "Vložte pouze číslice oddělené čárkami." msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "Hodnota musí být %(limit_value)s (nyní je %(show_value)s)." -#: core/validators.py:153 forms/fields.py:205 forms/fields.py:257 +#: core/validators.py:153 forms/fields.py:204 forms/fields.py:256 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Hodnota musí být menší nebo rovna %(limit_value)s." -#: core/validators.py:158 forms/fields.py:206 forms/fields.py:258 +#: core/validators.py:158 forms/fields.py:205 forms/fields.py:257 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Hodnota musí být větší nebo rovna %(limit_value)s." @@ -4529,13 +4541,13 @@ msgstr "" "Hodnota smí mít nejvýše %(limit_value)d znaků, ale nyní jich má %(show_value)" "d." -#: db/models/base.py:822 +#: db/models/base.py:823 #, python-format msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "" "Pole %(field_name)s musí být unikátní testem %(lookup)s pole %(date_field)s." -#: db/models/base.py:837 db/models/base.py:845 +#: db/models/base.py:838 db/models/base.py:846 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "" @@ -4559,13 +4571,13 @@ msgstr "Pole nemůže být prázdné." msgid "Field of type: %(field_type)s" msgstr "Pole typu: %(field_type)s" -#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:852 -#: db/models/fields/__init__.py:961 db/models/fields/__init__.py:972 -#: db/models/fields/__init__.py:999 +#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:860 +#: db/models/fields/__init__.py:969 db/models/fields/__init__.py:980 +#: db/models/fields/__init__.py:1007 msgid "Integer" msgstr "Celé číslo" -#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:850 +#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:858 msgid "This value must be an integer." msgstr "Hodnota musí být celé číslo." @@ -4577,7 +4589,7 @@ msgstr "Hodnota musí být buď Ano (True) nebo Ne (False)." msgid "Boolean (Either True or False)" msgstr "Pravdivost (buď Ano (True), nebo Ne (False))" -#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:982 +#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:990 #, python-format msgid "String (up to %(max_length)s)" msgstr "Řetězec (max. %(max_length)s znaků)" @@ -4619,44 +4631,44 @@ msgstr "Desetinné číslo" msgid "E-mail address" msgstr "E-mailová adresa" -#: db/models/fields/__init__.py:799 db/models/fields/files.py:220 +#: db/models/fields/__init__.py:807 db/models/fields/files.py:220 #: db/models/fields/files.py:331 msgid "File path" msgstr "Cesta k souboru" -#: db/models/fields/__init__.py:822 +#: db/models/fields/__init__.py:830 msgid "This value must be a float." msgstr "Hodnota musí být desetinné číslo." -#: db/models/fields/__init__.py:824 +#: db/models/fields/__init__.py:832 msgid "Floating point number" msgstr "Číslo s pohyblivou řádovou čárkou" -#: db/models/fields/__init__.py:883 +#: db/models/fields/__init__.py:891 msgid "Big (8 byte) integer" msgstr "Velké číslo (8 bajtů)" -#: db/models/fields/__init__.py:912 +#: db/models/fields/__init__.py:920 msgid "This value must be either None, True or False." msgstr "Hodnota musí být buď Nic (None), Ano (True) nebo Ne (False)." -#: db/models/fields/__init__.py:914 +#: db/models/fields/__init__.py:922 msgid "Boolean (Either True, False or None)" msgstr "Pravdivost (buď Ano (True), Ne (False) nebo Nic (None))" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1013 msgid "Text" msgstr "Text" -#: db/models/fields/__init__.py:1021 +#: db/models/fields/__init__.py:1029 msgid "Time" msgstr "Čas" -#: db/models/fields/__init__.py:1025 +#: db/models/fields/__init__.py:1033 msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." msgstr "Vložte platný čas ve tvaru HH:MM[:ss[.uuuuuu]]" -#: db/models/fields/__init__.py:1109 +#: db/models/fields/__init__.py:1125 msgid "XML text" msgstr "XML text" @@ -4669,22 +4681,22 @@ msgstr "Položka typu %(model)s s primárním klíčem %(pk)r neexistuje." msgid "Foreign Key (type determined by related field)" msgstr "Cizí klíč (typ určen pomocí souvisejícího pole)" -#: db/models/fields/related.py:918 +#: db/models/fields/related.py:919 msgid "One-to-one relationship" msgstr "Vazba jedna-jedna" -#: db/models/fields/related.py:980 +#: db/models/fields/related.py:981 msgid "Many-to-many relationship" msgstr "Vazba mnoho-mnoho" -#: db/models/fields/related.py:1000 +#: db/models/fields/related.py:1001 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." msgstr "" "Výběr více než jedné položky je možný přidržením klávesy \"Control\" (nebo " "\"Command\" na Macu)." -#: db/models/fields/related.py:1061 +#: db/models/fields/related.py:1062 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." msgid_plural "" @@ -4697,74 +4709,74 @@ msgstr[2] "Vložte platné ID položky %(self)s. Hodnoty %(value)r jsou neplatn msgid "This field is required." msgstr "Pole je povinné." -#: forms/fields.py:204 +#: forms/fields.py:203 msgid "Enter a whole number." msgstr "Vložte celé číslo." -#: forms/fields.py:235 forms/fields.py:256 +#: forms/fields.py:234 forms/fields.py:255 msgid "Enter a number." msgstr "Vložte číslo." -#: forms/fields.py:259 +#: forms/fields.py:258 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Hodnota nesmí celkem mít více než %s cifer." -#: forms/fields.py:260 +#: forms/fields.py:259 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Hodnota nesmí mít za desetinnou čárkou více než %s cifer." -#: forms/fields.py:261 +#: forms/fields.py:260 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Hodnota nesmí mít před desetinnou čárkou více než %s cifer." -#: forms/fields.py:323 forms/fields.py:838 +#: forms/fields.py:322 forms/fields.py:837 msgid "Enter a valid date." msgstr "Vložte platné datum." -#: forms/fields.py:351 forms/fields.py:839 +#: forms/fields.py:350 forms/fields.py:838 msgid "Enter a valid time." msgstr "Vložte platný čas." -#: forms/fields.py:377 +#: forms/fields.py:376 msgid "Enter a valid date/time." msgstr "Vložte platné datum a čas." -#: forms/fields.py:435 +#: forms/fields.py:434 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "Soubor nebyl odeslán. Zkontrolujte parametr \"encoding type\" formuláře." -#: forms/fields.py:436 +#: forms/fields.py:435 msgid "No file was submitted." msgstr "Žádný soubor nebyl odeslán." -#: forms/fields.py:437 +#: forms/fields.py:436 msgid "The submitted file is empty." msgstr "Odeslaný soubor je prázdný." -#: forms/fields.py:438 +#: forms/fields.py:437 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr "" "Délka názvu souboru má být nejvýše %(max)d znaků, ale nyní je %(length)d." -#: forms/fields.py:473 +#: forms/fields.py:472 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" "Nahrajte platný obrázek. Odeslaný soubor buď nebyl obrázek nebo byl poškozen." -#: forms/fields.py:596 forms/fields.py:671 +#: forms/fields.py:595 forms/fields.py:670 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Vyberte platnou možnost, \"%(value)s\" není k dispozici." -#: forms/fields.py:672 forms/fields.py:734 forms/models.py:1002 +#: forms/fields.py:671 forms/fields.py:733 forms/models.py:1002 msgid "Enter a list of values." msgstr "Vložte seznam hodnot." @@ -4828,18 +4840,18 @@ msgstr[2] "%(size)d bajtů" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/cs/LC_MESSAGES/djangojs.mo b/django/conf/locale/cs/LC_MESSAGES/djangojs.mo index 1cf448fe723f..8efa6f6e4cfb 100644 Binary files a/django/conf/locale/cs/LC_MESSAGES/djangojs.mo and b/django/conf/locale/cs/LC_MESSAGES/djangojs.mo differ diff --git a/django/conf/locale/cs/LC_MESSAGES/djangojs.po b/django/conf/locale/cs/LC_MESSAGES/djangojs.po index 0b958e290cc3..e4f06a96ede8 100644 --- a/django/conf/locale/cs/LC_MESSAGES/djangojs.po +++ b/django/conf/locale/cs/LC_MESSAGES/djangojs.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-09 14:30+0200\n" -"PO-Revision-Date: 2010-05-09 14:04+0100\n" +"POT-Creation-Date: 2010-08-06 18:35+0200\n" +"PO-Revision-Date: 2010-08-06 18:34+0100\n" "Last-Translator: Vlada Macek \n" "Language-Team: Czech\n" "MIME-Version: 1.0\n" diff --git a/django/conf/locale/da/LC_MESSAGES/django.mo b/django/conf/locale/da/LC_MESSAGES/django.mo index 99f6002aee41..9ce8c9ca55da 100644 Binary files a/django/conf/locale/da/LC_MESSAGES/django.mo and b/django/conf/locale/da/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/da/LC_MESSAGES/django.po b/django/conf/locale/da/LC_MESSAGES/django.po index 63526f940ea1..40b881935232 100644 --- a/django/conf/locale/da/LC_MESSAGES/django.po +++ b/django/conf/locale/da/LC_MESSAGES/django.po @@ -4798,18 +4798,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/da/LC_MESSAGES/djangojs.mo b/django/conf/locale/da/LC_MESSAGES/djangojs.mo index 697f1a9f6a80..47490d86205c 100644 Binary files a/django/conf/locale/da/LC_MESSAGES/djangojs.mo and b/django/conf/locale/da/LC_MESSAGES/djangojs.mo differ diff --git a/django/conf/locale/da/LC_MESSAGES/djangojs.po b/django/conf/locale/da/LC_MESSAGES/djangojs.po index 4db760129414..f220938ae1e4 100644 --- a/django/conf/locale/da/LC_MESSAGES/djangojs.po +++ b/django/conf/locale/da/LC_MESSAGES/djangojs.po @@ -6,9 +6,9 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-04-26 15:49+0200\n" -"PO-Revision-Date: 2008-08-13 22:00+0200\n" -"Last-Translator: Finn Gruwier Larsen\n" +"POT-Creation-Date: 2010-08-07 11:57+0200\n" +"PO-Revision-Date: 2010-08-07 22:00+0200\n" +"Last-Translator: Finn Gruwier Larsen\n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -44,13 +44,42 @@ msgstr "Foretag dit/dine valg og klik " msgid "Clear all" msgstr "Fravælg alle" -#: contrib/admin/media/js/actions.js:17 +#: contrib/admin/media/js/actions.js:18 #: contrib/admin/media/js/actions.min.js:1 msgid "%(sel)s of %(cnt)s selected" msgid_plural "%(sel)s of %(cnt)s selected" msgstr[0] "%(sel)s af %(cnt)s valgt" msgstr[1] "%(sel)s af %(cnt)s valgt" +#: contrib/admin/media/js/actions.js:109 +#: contrib/admin/media/js/actions.min.js:5 +msgid "" +"You have unsaved changes on individual editable fields. If you run an " +"action, your unsaved changes will be lost." +msgstr "" +"Du har ugemte ændringer af et eller flere redigerbare felter. Hvis du " +"udfører en handling fra drop-down-menuen, vil du miste disse ændringer." + +#: contrib/admin/media/js/actions.js:121 +#: contrib/admin/media/js/actions.min.js:6 +msgid "" +"You have selected an action, but you haven't saved your changes to " +"individual fields yet. Please click OK to save. You'll need to re-run the " +"action." +msgstr "" +"Du har valgt en handling, men du har ikke gemt dine ændringer til et eller " +"flere felter. Klik venligst OK for at gemme og vælg dernæst handlingen igen." + +#: contrib/admin/media/js/actions.js:123 +#: contrib/admin/media/js/actions.min.js:6 +msgid "" +"You have selected an action, and you haven't made any changes on individual " +"fields. You're probably looking for the Go button rather than the Save " +"button." +msgstr "" +"Du har valgt en handling, og du har ikke udført nogen ændringer på felter. " +"Det, du søger er formentlig Udfør-knappen i stedet for Gem-knappen." + #: contrib/admin/media/js/calendar.js:24 #: contrib/admin/media/js/dateparse.js:32 msgid "" diff --git a/django/conf/locale/de/LC_MESSAGES/django.mo b/django/conf/locale/de/LC_MESSAGES/django.mo index 5f88e60a6894..14b14775d4e4 100644 Binary files a/django/conf/locale/de/LC_MESSAGES/django.mo and b/django/conf/locale/de/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/de/LC_MESSAGES/django.po b/django/conf/locale/de/LC_MESSAGES/django.po index 521bd3c89c49..0da9d392340c 100644 --- a/django/conf/locale/de/LC_MESSAGES/django.po +++ b/django/conf/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-13 15:30+0200\n" +"POT-Creation-Date: 2010-08-06 18:48+0200\n" "PO-Revision-Date: 2010-04-26 13:53+0100\n" "Last-Translator: Jannis Leidel \n" "Language-Team: \n" @@ -72,7 +72,7 @@ msgid "Spanish" msgstr "Spanisch" #: conf/global_settings.py:57 -msgid "Argentinean Spanish" +msgid "Argentinian Spanish" msgstr "Argentinisches Spanisch" #: conf/global_settings.py:58 @@ -168,98 +168,102 @@ msgid "Macedonian" msgstr "Mazedonisch" #: conf/global_settings.py:81 +msgid "Malayalam" +msgstr "Malayalam" + +#: conf/global_settings.py:82 msgid "Mongolian" msgstr "Mongolisch" -#: conf/global_settings.py:82 +#: conf/global_settings.py:83 msgid "Dutch" msgstr "Holländisch" -#: conf/global_settings.py:83 +#: conf/global_settings.py:84 msgid "Norwegian" msgstr "Norwegisch" -#: conf/global_settings.py:84 +#: conf/global_settings.py:85 msgid "Norwegian Bokmal" msgstr "Norwegisch (Bokmål)" -#: conf/global_settings.py:85 +#: conf/global_settings.py:86 msgid "Norwegian Nynorsk" msgstr "Norwegisch (Nynorsk)" -#: conf/global_settings.py:86 +#: conf/global_settings.py:87 msgid "Polish" msgstr "Polnisch" -#: conf/global_settings.py:87 +#: conf/global_settings.py:88 msgid "Portuguese" msgstr "Portugiesisch" -#: conf/global_settings.py:88 +#: conf/global_settings.py:89 msgid "Brazilian Portuguese" msgstr "Brasilianisches Portugiesisch" -#: conf/global_settings.py:89 +#: conf/global_settings.py:90 msgid "Romanian" msgstr "Rumänisch" -#: conf/global_settings.py:90 +#: conf/global_settings.py:91 msgid "Russian" msgstr "Russisch" -#: conf/global_settings.py:91 +#: conf/global_settings.py:92 msgid "Slovak" msgstr "Slowakisch" -#: conf/global_settings.py:92 +#: conf/global_settings.py:93 msgid "Slovenian" msgstr "Slowenisch" -#: conf/global_settings.py:93 +#: conf/global_settings.py:94 msgid "Albanian" msgstr "Albanisch" -#: conf/global_settings.py:94 +#: conf/global_settings.py:95 msgid "Serbian" msgstr "Serbisch" -#: conf/global_settings.py:95 +#: conf/global_settings.py:96 msgid "Serbian Latin" msgstr "Serbisch (Latein)" -#: conf/global_settings.py:96 +#: conf/global_settings.py:97 msgid "Swedish" msgstr "Schwedisch" -#: conf/global_settings.py:97 +#: conf/global_settings.py:98 msgid "Tamil" msgstr "Tamilisch" -#: conf/global_settings.py:98 +#: conf/global_settings.py:99 msgid "Telugu" msgstr "Telugisch" -#: conf/global_settings.py:99 +#: conf/global_settings.py:100 msgid "Thai" msgstr "Thailändisch" -#: conf/global_settings.py:100 +#: conf/global_settings.py:101 msgid "Turkish" msgstr "Türkisch" -#: conf/global_settings.py:101 +#: conf/global_settings.py:102 msgid "Ukrainian" msgstr "Ukrainisch" -#: conf/global_settings.py:102 +#: conf/global_settings.py:103 msgid "Vietnamese" msgstr "Vietnamesisch" -#: conf/global_settings.py:103 +#: conf/global_settings.py:104 msgid "Simplified Chinese" msgstr "Vereinfachtes Chinesisch" -#: conf/global_settings.py:104 +#: conf/global_settings.py:105 msgid "Traditional Chinese" msgstr "Traditionelles Chinesisch" @@ -311,15 +315,15 @@ msgstr "Diesen Monat" msgid "This year" msgstr "Dieses Jahr" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "Yes" msgstr "Ja" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "No" msgstr "Nein" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:478 msgid "Unknown" msgstr "Unbekannt" @@ -859,7 +863,7 @@ msgstr "Sichern und neu hinzufügen" msgid "Save and continue editing" msgstr "Sichern und weiter bearbeiten" -#: contrib/admin/templates/admin/auth/user/add_form.html:5 +#: contrib/admin/templates/admin/auth/user/add_form.html:6 msgid "" "First, enter a username and password. Then, you'll be able to edit more user " "options." @@ -867,6 +871,10 @@ msgstr "" "Zuerst einen Benutzer und ein Passwort eingeben. Danach können weitere " "Optionen für den Benutzer geändert werden." +#: contrib/admin/templates/admin/auth/user/add_form.html:8 +msgid "Enter a username and password." +msgstr "Bitte einen Benutzernamen und ein Passwort eingeben." + #: contrib/admin/templates/admin/auth/user/change_password.html:28 #, python-format msgid "Enter a new password for the user %(username)s." @@ -1437,8 +1445,8 @@ msgstr "Mitteilung" msgid "Logged out" msgstr "Abgemeldet" -#: contrib/auth/management/commands/createsuperuser.py:23 -#: core/validators.py:120 forms/fields.py:428 +#: contrib/auth/management/commands/createsuperuser.py:24 +#: core/validators.py:120 forms/fields.py:427 msgid "Enter a valid e-mail address." msgstr "Bitte eine gültige E-Mail-Adresse eingeben." @@ -1506,7 +1514,7 @@ msgid "Email address" msgstr "E-Mail-Adresse" #: contrib/comments/forms.py:95 contrib/flatpages/admin.py:8 -#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1101 +#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1109 msgid "URL" msgstr "Adresse (URL)" @@ -1557,7 +1565,7 @@ msgstr "Kommentar" msgid "date/time submitted" msgstr "Datum/Zeit Erstellung" -#: contrib/comments/models.py:60 db/models/fields/__init__.py:896 +#: contrib/comments/models.py:60 db/models/fields/__init__.py:904 msgid "IP address" msgstr "IP-Adresse" @@ -4509,22 +4517,22 @@ msgstr "Sites" msgid "Enter a valid value." msgstr "Bitte einen gültigen Wert eingeben." -#: core/validators.py:87 forms/fields.py:529 +#: core/validators.py:87 forms/fields.py:528 msgid "Enter a valid URL." msgstr "Bitte eine gültige Adresse eingeben." -#: core/validators.py:89 forms/fields.py:530 +#: core/validators.py:89 forms/fields.py:529 msgid "This URL appears to be a broken link." msgstr "Diese Adresse scheint nicht gültig zu sein." -#: core/validators.py:123 forms/fields.py:873 +#: core/validators.py:123 forms/fields.py:877 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Bitte ein gültiges Kürzel, bestehend aus Buchstaben, Ziffern, Unterstrichen " "und Bindestrichen, eingeben." -#: core/validators.py:126 forms/fields.py:866 +#: core/validators.py:126 forms/fields.py:870 msgid "Enter a valid IPv4 address." msgstr "Bitte eine gültige IPv4-Adresse eingeben." @@ -4539,12 +4547,12 @@ msgstr "" "Bitte sicherstellen, dass der Wert %(limit_value)s ist. (Er ist %(show_value)" "s)" -#: core/validators.py:153 forms/fields.py:205 forms/fields.py:257 +#: core/validators.py:153 forms/fields.py:204 forms/fields.py:256 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Dieser Wert muss kleiner oder gleich %(limit_value)s sein." -#: core/validators.py:158 forms/fields.py:206 forms/fields.py:258 +#: core/validators.py:158 forms/fields.py:205 forms/fields.py:257 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Dieser Wert muss größer oder gleich %(limit_value)s sein." @@ -4595,13 +4603,13 @@ msgstr "Dieses Feld darf nicht leer sein." msgid "Field of type: %(field_type)s" msgstr "Feldtyp: %(field_type)s" -#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:852 -#: db/models/fields/__init__.py:961 db/models/fields/__init__.py:972 -#: db/models/fields/__init__.py:999 +#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:860 +#: db/models/fields/__init__.py:969 db/models/fields/__init__.py:980 +#: db/models/fields/__init__.py:1007 msgid "Integer" msgstr "Ganzzahl" -#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:850 +#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:858 msgid "This value must be an integer." msgstr "Dieser Wert muss eine Ganzzahl sein." @@ -4613,7 +4621,7 @@ msgstr "Dieser Wert muss True oder False sein." msgid "Boolean (Either True or False)" msgstr "Boolescher Wert (True oder False)" -#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:982 +#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:990 #, python-format msgid "String (up to %(max_length)s)" msgstr "Zeichenkette (bis zu %(max_length)s Zeichen)" @@ -4657,44 +4665,44 @@ msgstr "Dezimalzahl" msgid "E-mail address" msgstr "E-Mail-Adresse" -#: db/models/fields/__init__.py:799 db/models/fields/files.py:220 +#: db/models/fields/__init__.py:807 db/models/fields/files.py:220 #: db/models/fields/files.py:331 msgid "File path" msgstr "Dateipfad" -#: db/models/fields/__init__.py:822 +#: db/models/fields/__init__.py:830 msgid "This value must be a float." msgstr "Dieser Wert muss eine Gleitkommazahl sein." -#: db/models/fields/__init__.py:824 +#: db/models/fields/__init__.py:832 msgid "Floating point number" msgstr "Gleitkommazahl" -#: db/models/fields/__init__.py:883 +#: db/models/fields/__init__.py:891 msgid "Big (8 byte) integer" msgstr "Große Ganzzahl (8 Byte)" -#: db/models/fields/__init__.py:912 +#: db/models/fields/__init__.py:920 msgid "This value must be either None, True or False." msgstr "Dieser Wert muss None, True oder False sein." -#: db/models/fields/__init__.py:914 +#: db/models/fields/__init__.py:922 msgid "Boolean (Either True, False or None)" msgstr "Boolescher Wert (True, False oder None)" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1013 msgid "Text" msgstr "Text" -#: db/models/fields/__init__.py:1021 +#: db/models/fields/__init__.py:1029 msgid "Time" msgstr "Zeit" -#: db/models/fields/__init__.py:1025 +#: db/models/fields/__init__.py:1033 msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." msgstr "Bitte eine gültige Zeit im Format HH:MM[:ss[.uuuuuu]] eingeben." -#: db/models/fields/__init__.py:1109 +#: db/models/fields/__init__.py:1125 msgid "XML text" msgstr "XML-Text" @@ -4707,22 +4715,22 @@ msgstr "Modell %(model)s mit dem Primärschlüssel %(pk)r ist nicht vorhanden." msgid "Foreign Key (type determined by related field)" msgstr "Fremdschlüssel (Typ definiert durch verknüpftes Feld)" -#: db/models/fields/related.py:918 +#: db/models/fields/related.py:919 msgid "One-to-one relationship" msgstr "One-to-one-Beziehung" -#: db/models/fields/related.py:980 +#: db/models/fields/related.py:981 msgid "Many-to-many relationship" msgstr "Many-to-many-Beziehung" -#: db/models/fields/related.py:1000 +#: db/models/fields/related.py:1001 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." msgstr "" "Halten Sie die Strg-Taste (⌘ für Mac) während des Klickens gedrückt, um " "mehrere Einträge auszuwählen." -#: db/models/fields/related.py:1061 +#: db/models/fields/related.py:1062 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." msgid_plural "" @@ -4736,55 +4744,55 @@ msgstr[1] "" msgid "This field is required." msgstr "Dieses Feld ist zwingend erforderlich." -#: forms/fields.py:204 +#: forms/fields.py:203 msgid "Enter a whole number." msgstr "Bitte eine ganze Zahl eingeben." -#: forms/fields.py:235 forms/fields.py:256 +#: forms/fields.py:234 forms/fields.py:255 msgid "Enter a number." msgstr "Bitte eine Zahl eingeben." -#: forms/fields.py:259 +#: forms/fields.py:258 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Bitte geben Sie nicht mehr als insgesamt %s Ziffern ein." -#: forms/fields.py:260 +#: forms/fields.py:259 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Bitte geben Sie nicht mehr als %s Dezimalstellen ein." -#: forms/fields.py:261 +#: forms/fields.py:260 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Bitte geben Sie nicht mehr als %s Ziffern vor dem Komma ein." -#: forms/fields.py:323 forms/fields.py:838 +#: forms/fields.py:322 forms/fields.py:837 msgid "Enter a valid date." msgstr "Bitte ein gültiges Datum eingeben." -#: forms/fields.py:351 forms/fields.py:839 +#: forms/fields.py:350 forms/fields.py:838 msgid "Enter a valid time." msgstr "Bitte eine gültige Uhrzeit eingeben." -#: forms/fields.py:377 +#: forms/fields.py:376 msgid "Enter a valid date/time." msgstr "Bitte ein gültiges Datum und Uhrzeit eingeben." -#: forms/fields.py:435 +#: forms/fields.py:434 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "Es wurde keine Datei übermittelt. Überprüfen Sie das Encoding des Formulars." -#: forms/fields.py:436 +#: forms/fields.py:435 msgid "No file was submitted." msgstr "Es wurde keine Datei übertragen." -#: forms/fields.py:437 +#: forms/fields.py:436 msgid "The submitted file is empty." msgstr "Die ausgewählte Datei ist leer." -#: forms/fields.py:438 +#: forms/fields.py:437 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -4792,7 +4800,7 @@ msgstr "" "Bitte sicherstellen, dass der Dateiname maximal %(max)d Zeichen hat. (Er hat " "%(length)d)." -#: forms/fields.py:473 +#: forms/fields.py:472 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -4800,13 +4808,13 @@ msgstr "" "Bitte ein Bild hochladen. Die hochgeladene Datei ist kein Bild oder ist " "defekt." -#: forms/fields.py:596 forms/fields.py:671 +#: forms/fields.py:595 forms/fields.py:670 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Bitte eine gültige Auswahl treffen. %(value)s ist keine gültige Auswahl." -#: forms/fields.py:672 forms/fields.py:734 forms/models.py:1002 +#: forms/fields.py:671 forms/fields.py:733 forms/models.py:1002 msgid "Enter a list of values." msgstr "Bitte eine Liste mit Werten eingeben." @@ -4871,18 +4879,18 @@ msgstr[1] "%(size)d Bytes" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/en/LC_MESSAGES/django.po b/django/conf/locale/en/LC_MESSAGES/django.po index c47843211ba2..dc13bb578012 100644 --- a/django/conf/locale/en/LC_MESSAGES/django.po +++ b/django/conf/locale/en/LC_MESSAGES/django.po @@ -4695,17 +4695,17 @@ msgstr[1] "" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" +msgid "%s KB" msgstr "" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" +msgid "%s MB" msgstr "" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" +msgid "%s GB" msgstr "" #: utils/dateformat.py:42 diff --git a/django/conf/locale/en/formats.py b/django/conf/locale/en/formats.py index 141f705fb9b1..c59a94a1c77d 100644 --- a/django/conf/locale/en/formats.py +++ b/django/conf/locale/en/formats.py @@ -32,7 +32,7 @@ '%m/%d/%y %H:%M', # '10/25/06 14:30' '%m/%d/%y', # '10/25/06' ) -DECIMAL_SEPARATOR = '.' -THOUSAND_SEPARATOR = ',' +DECIMAL_SEPARATOR = u'.' +THOUSAND_SEPARATOR = u',' NUMBER_GROUPING = 3 diff --git a/django/conf/locale/en_GB/LC_MESSAGES/django.mo b/django/conf/locale/en_GB/LC_MESSAGES/django.mo index 7252fa25ae70..b636411a4e99 100644 Binary files a/django/conf/locale/en_GB/LC_MESSAGES/django.mo and b/django/conf/locale/en_GB/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/en_GB/LC_MESSAGES/django.po b/django/conf/locale/en_GB/LC_MESSAGES/django.po index 226824a46be4..0269456de3c1 100644 --- a/django/conf/locale/en_GB/LC_MESSAGES/django.po +++ b/django/conf/locale/en_GB/LC_MESSAGES/django.po @@ -4689,17 +4689,17 @@ msgstr[1] "" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" +msgid "%s KB" msgstr "" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" +msgid "%s MB" msgstr "" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" +msgid "%s GB" msgstr "" #: utils/dateformat.py:42 diff --git a/django/conf/locale/es/LC_MESSAGES/django.mo b/django/conf/locale/es/LC_MESSAGES/django.mo index e456b629eb4b..d359470aa7d7 100644 Binary files a/django/conf/locale/es/LC_MESSAGES/django.mo and b/django/conf/locale/es/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/es/LC_MESSAGES/django.po b/django/conf/locale/es/LC_MESSAGES/django.po index 90136b611f50..c825b1c4bb4d 100644 --- a/django/conf/locale/es/LC_MESSAGES/django.po +++ b/django/conf/locale/es/LC_MESSAGES/django.po @@ -4832,18 +4832,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/es_AR/LC_MESSAGES/django.mo b/django/conf/locale/es_AR/LC_MESSAGES/django.mo index 1aee7ccc22df..6a03dfc672e1 100644 Binary files a/django/conf/locale/es_AR/LC_MESSAGES/django.mo and b/django/conf/locale/es_AR/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/es_AR/LC_MESSAGES/django.po b/django/conf/locale/es_AR/LC_MESSAGES/django.po index 061cd7d1ba5f..0875aeec1837 100644 --- a/django/conf/locale/es_AR/LC_MESSAGES/django.po +++ b/django/conf/locale/es_AR/LC_MESSAGES/django.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-11 08:35-0300\n" -"PO-Revision-Date: 2010-05-17 10:52-0300\n" +"POT-Creation-Date: 2010-08-06 16:06-0300\n" +"PO-Revision-Date: 2010-09-10 16:42-0300\n" "Last-Translator: Ramiro \n" "Language-Team: Django-I18N \n" "Language: es_AR\n" @@ -70,7 +70,7 @@ msgid "Spanish" msgstr "español" #: conf/global_settings.py:57 -msgid "Argentinean Spanish" +msgid "Argentinian Spanish" msgstr "español de Argentina" #: conf/global_settings.py:58 @@ -166,98 +166,102 @@ msgid "Macedonian" msgstr "macedonio" #: conf/global_settings.py:81 +msgid "Malayalam" +msgstr "Malayalam" + +#: conf/global_settings.py:82 msgid "Mongolian" msgstr "mongol" -#: conf/global_settings.py:82 +#: conf/global_settings.py:83 msgid "Dutch" msgstr "holandés" -#: conf/global_settings.py:83 +#: conf/global_settings.py:84 msgid "Norwegian" msgstr "noruego" -#: conf/global_settings.py:84 +#: conf/global_settings.py:85 msgid "Norwegian Bokmal" msgstr "bokmål" -#: conf/global_settings.py:85 +#: conf/global_settings.py:86 msgid "Norwegian Nynorsk" msgstr "nynorsk" -#: conf/global_settings.py:86 +#: conf/global_settings.py:87 msgid "Polish" msgstr "polaco" -#: conf/global_settings.py:87 +#: conf/global_settings.py:88 msgid "Portuguese" msgstr "portugués" -#: conf/global_settings.py:88 +#: conf/global_settings.py:89 msgid "Brazilian Portuguese" msgstr "portugués de Brasil" -#: conf/global_settings.py:89 +#: conf/global_settings.py:90 msgid "Romanian" msgstr "rumano" -#: conf/global_settings.py:90 +#: conf/global_settings.py:91 msgid "Russian" msgstr "ruso" -#: conf/global_settings.py:91 +#: conf/global_settings.py:92 msgid "Slovak" msgstr "eslovaco" -#: conf/global_settings.py:92 +#: conf/global_settings.py:93 msgid "Slovenian" msgstr "esloveno" -#: conf/global_settings.py:93 +#: conf/global_settings.py:94 msgid "Albanian" msgstr "albanés" -#: conf/global_settings.py:94 +#: conf/global_settings.py:95 msgid "Serbian" msgstr "serbio" -#: conf/global_settings.py:95 +#: conf/global_settings.py:96 msgid "Serbian Latin" msgstr "Latín de Serbia" -#: conf/global_settings.py:96 +#: conf/global_settings.py:97 msgid "Swedish" msgstr "sueco" -#: conf/global_settings.py:97 +#: conf/global_settings.py:98 msgid "Tamil" msgstr "tamil" -#: conf/global_settings.py:98 +#: conf/global_settings.py:99 msgid "Telugu" msgstr "telugu" -#: conf/global_settings.py:99 +#: conf/global_settings.py:100 msgid "Thai" msgstr "tailandés" -#: conf/global_settings.py:100 +#: conf/global_settings.py:101 msgid "Turkish" msgstr "turco" -#: conf/global_settings.py:101 +#: conf/global_settings.py:102 msgid "Ukrainian" msgstr "ucraniano" -#: conf/global_settings.py:102 +#: conf/global_settings.py:103 msgid "Vietnamese" msgstr "vietnamita" -#: conf/global_settings.py:103 +#: conf/global_settings.py:104 msgid "Simplified Chinese" msgstr "chino simplificado" -#: conf/global_settings.py:104 +#: conf/global_settings.py:105 msgid "Traditional Chinese" msgstr "chino tradicional" @@ -309,15 +313,15 @@ msgstr "Este mes" msgid "This year" msgstr "Este año" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "Yes" msgstr "Sí" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "No" msgstr "No" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:478 msgid "Unknown" msgstr "Desconocido" @@ -363,7 +367,7 @@ msgid "Changed %s." msgstr "Modifica %s." #: contrib/admin/options.py:559 contrib/admin/options.py:569 -#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:844 +#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:845 #: forms/models.py:568 msgid "and" msgstr "y" @@ -855,13 +859,17 @@ msgstr "Guardar y agregar otro" msgid "Save and continue editing" msgstr "Guardar y continuar editando" -#: contrib/admin/templates/admin/auth/user/add_form.html:5 +#: contrib/admin/templates/admin/auth/user/add_form.html:6 msgid "" "First, enter a username and password. Then, you'll be able to edit more user " "options." msgstr "" -"Primero, introduzca un nombre de usuario y una contraseña. Luego podrá " -"configurar opciones adicionales." +"Primero introduzca un nombre de usuario y una contraseña. Luego podrá " +"configurar opciones adicionales acerca del usuario." + +#: contrib/admin/templates/admin/auth/user/add_form.html:8 +msgid "Enter a username and password." +msgstr "Introduzca un nombre de usuario y una contraseña." #: contrib/admin/templates/admin/auth/user/change_password.html:28 #, python-format @@ -873,7 +881,7 @@ msgstr "" #: contrib/admin/templates/admin/auth/user/change_password.html:35 #: contrib/auth/forms.py:17 contrib/auth/forms.py:61 contrib/auth/forms.py:186 msgid "Password" -msgstr "Contraseña:" +msgstr "Contraseña" #: contrib/admin/templates/admin/auth/user/change_password.html:41 #: contrib/admin/templates/registration/password_change_form.html:37 @@ -1243,7 +1251,7 @@ msgstr "Cambiar contraseña: %s" #: contrib/auth/forms.py:14 contrib/auth/forms.py:48 contrib/auth/forms.py:60 msgid "Username" -msgstr "Nombre de usuario:" +msgstr "Nombre de usuario" #: contrib/auth/forms.py:15 contrib/auth/forms.py:49 msgid "Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only." @@ -1435,8 +1443,8 @@ msgstr "mensaje" msgid "Logged out" msgstr "Sesión cerrada" -#: contrib/auth/management/commands/createsuperuser.py:23 -#: core/validators.py:120 forms/fields.py:428 +#: contrib/auth/management/commands/createsuperuser.py:24 +#: core/validators.py:120 forms/fields.py:427 msgid "Enter a valid e-mail address." msgstr "Introduzca una dirección de correo electrónico válida" @@ -1504,7 +1512,7 @@ msgid "Email address" msgstr "Dirección de correo electrónico" #: contrib/comments/forms.py:95 contrib/flatpages/admin.py:8 -#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1101 +#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1109 msgid "URL" msgstr "URL" @@ -1553,7 +1561,7 @@ msgstr "comentario" msgid "date/time submitted" msgstr "fecha/hora de envío" -#: contrib/comments/models.py:60 db/models/fields/__init__.py:896 +#: contrib/comments/models.py:60 db/models/fields/__init__.py:904 msgid "IP address" msgstr "Dirección IP" @@ -4503,20 +4511,20 @@ msgstr "sitios" msgid "Enter a valid value." msgstr "Introduzca un valor válido." -#: core/validators.py:87 forms/fields.py:529 +#: core/validators.py:87 forms/fields.py:528 msgid "Enter a valid URL." msgstr "Introduzca una URL válida." -#: core/validators.py:89 forms/fields.py:530 +#: core/validators.py:89 forms/fields.py:529 msgid "This URL appears to be a broken link." msgstr "La URL parece ser un enlace roto." -#: core/validators.py:123 forms/fields.py:873 +#: core/validators.py:123 forms/fields.py:877 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "Introduzca un 'slug' válido consistente de letras, números o guiones." -#: core/validators.py:126 forms/fields.py:866 +#: core/validators.py:126 forms/fields.py:870 msgid "Enter a valid IPv4 address." msgstr "Introduzca una dirección IPv4 válida" @@ -4528,15 +4536,15 @@ msgstr "Introduzca sólo dígitos separados por comas." #, python-format msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "" -"Asegúrese de que este valor sea %(limit_value)s (actualmente es %(show_value)" -"s)." +"Asegúrese de que este valor sea %(limit_value)s (actualmente es " +"%(show_value)s)." -#: core/validators.py:153 forms/fields.py:205 forms/fields.py:257 +#: core/validators.py:153 forms/fields.py:204 forms/fields.py:256 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Asegúrese de que este valor sea menor o igual a %(limit_value)s." -#: core/validators.py:158 forms/fields.py:206 forms/fields.py:258 +#: core/validators.py:158 forms/fields.py:205 forms/fields.py:257 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Asegúrese de que este valor sea mayor o igual a %(limit_value)s." @@ -4544,8 +4552,8 @@ msgstr "Asegúrese de que este valor sea mayor o igual a %(limit_value)s." #: core/validators.py:164 #, python-format msgid "" -"Ensure this value has at least %(limit_value)d characters (it has %" -"(show_value)d)." +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." msgstr "" "Asegúrese de que este valor tenga al menos %(limit_value)d caracteres (tiene " "%(show_value)d)." @@ -4553,20 +4561,20 @@ msgstr "" #: core/validators.py:170 #, python-format msgid "" -"Ensure this value has at most %(limit_value)d characters (it has %" -"(show_value)d)." +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." msgstr "" "Asegúrese de que este valor tenga como máximo %(limit_value)d caracteres " "(tiene %(show_value)d)." -#: db/models/base.py:822 +#: db/models/base.py:823 #, python-format msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "" "%(field_name)s debe ser único/a para un %(lookup)s %(date_field)s " "determinado." -#: db/models/base.py:837 db/models/base.py:845 +#: db/models/base.py:838 db/models/base.py:846 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "Ya existe un/a %(model_name)s con este/a %(field_label)s." @@ -4589,13 +4597,13 @@ msgstr "Este campo no puede estar en blanco." msgid "Field of type: %(field_type)s" msgstr "Campo tipo: %(field_type)s" -#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:852 -#: db/models/fields/__init__.py:961 db/models/fields/__init__.py:972 -#: db/models/fields/__init__.py:999 +#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:860 +#: db/models/fields/__init__.py:969 db/models/fields/__init__.py:980 +#: db/models/fields/__init__.py:1007 msgid "Integer" msgstr "Entero" -#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:850 +#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:858 msgid "This value must be an integer." msgstr "Este valor debe ser un número entero." @@ -4607,7 +4615,7 @@ msgstr "Este valor debe ser True o False." msgid "Boolean (Either True or False)" msgstr "Booleano (Verdadero o Falso)" -#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:982 +#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:990 #, python-format msgid "String (up to %(max_length)s)" msgstr "Cadena (máximo %(max_length)s)" @@ -4651,44 +4659,44 @@ msgstr "Número decimal" msgid "E-mail address" msgstr "Dirección de correo electrónico" -#: db/models/fields/__init__.py:799 db/models/fields/files.py:220 +#: db/models/fields/__init__.py:807 db/models/fields/files.py:220 #: db/models/fields/files.py:331 msgid "File path" msgstr "Ruta de archivo" -#: db/models/fields/__init__.py:822 +#: db/models/fields/__init__.py:830 msgid "This value must be a float." msgstr "Este valor debe ser un valor en representación de punto flotante." -#: db/models/fields/__init__.py:824 +#: db/models/fields/__init__.py:832 msgid "Floating point number" msgstr "Número de punto flotante" -#: db/models/fields/__init__.py:883 +#: db/models/fields/__init__.py:891 msgid "Big (8 byte) integer" msgstr "Entero grande (8 bytes)" -#: db/models/fields/__init__.py:912 +#: db/models/fields/__init__.py:920 msgid "This value must be either None, True or False." msgstr "Este valor debe ser None, True o False." -#: db/models/fields/__init__.py:914 +#: db/models/fields/__init__.py:922 msgid "Boolean (Either True, False or None)" msgstr "Booleano (Verdadero, Falso o Nulo)" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1013 msgid "Text" msgstr "Texto" -#: db/models/fields/__init__.py:1021 +#: db/models/fields/__init__.py:1029 msgid "Time" msgstr "Hora" -#: db/models/fields/__init__.py:1025 +#: db/models/fields/__init__.py:1033 msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." msgstr "Introduzca un valor de hora válido en formato HH:MM[:ss[.uuuuuu]]." -#: db/models/fields/__init__.py:1109 +#: db/models/fields/__init__.py:1125 msgid "XML text" msgstr "Texto XML" @@ -4701,22 +4709,22 @@ msgstr "No existe un modelo %(model)s con una clave primaria %(pk)r." msgid "Foreign Key (type determined by related field)" msgstr "Clave foránea (el tipo está determinado por el campo relacionado)" -#: db/models/fields/related.py:918 +#: db/models/fields/related.py:919 msgid "One-to-one relationship" msgstr "Relación uno-a-uno" -#: db/models/fields/related.py:980 +#: db/models/fields/related.py:981 msgid "Many-to-many relationship" msgstr "Relación muchos-a-muchos" -#: db/models/fields/related.py:1000 +#: db/models/fields/related.py:1001 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." msgstr "" "Mantenga presionada \"Control\" (\"Command\" en una Mac) para seleccionar " "más de uno." -#: db/models/fields/related.py:1061 +#: db/models/fields/related.py:1062 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." msgid_plural "" @@ -4732,55 +4740,55 @@ msgstr[1] "" msgid "This field is required." msgstr "Este campo es obligatorio." -#: forms/fields.py:204 +#: forms/fields.py:203 msgid "Enter a whole number." msgstr "Introduzca un número entero." -#: forms/fields.py:235 forms/fields.py:256 +#: forms/fields.py:234 forms/fields.py:255 msgid "Enter a number." msgstr "Introduzca un número." -#: forms/fields.py:259 +#: forms/fields.py:258 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Asegúrese de que no existan en total mas de %s dígitos." -#: forms/fields.py:260 +#: forms/fields.py:259 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Asegúrese de que no existan mas de %s lugares decimales." -#: forms/fields.py:261 +#: forms/fields.py:260 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Asegúrese de que no existan mas de %s dígitos antes del punto decimal." -#: forms/fields.py:323 forms/fields.py:838 +#: forms/fields.py:322 forms/fields.py:837 msgid "Enter a valid date." msgstr "Introduzca una fecha válida." -#: forms/fields.py:351 forms/fields.py:839 +#: forms/fields.py:350 forms/fields.py:838 msgid "Enter a valid time." msgstr "Introduzca un valor de hora válido." -#: forms/fields.py:377 +#: forms/fields.py:376 msgid "Enter a valid date/time." msgstr "Introduzca un valor de fecha/hora válido." -#: forms/fields.py:435 +#: forms/fields.py:434 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "No se envió un archivo. Verifique el tipo de codificación en el formulario." -#: forms/fields.py:436 +#: forms/fields.py:435 msgid "No file was submitted." msgstr "No se envió ningún archivo." -#: forms/fields.py:437 +#: forms/fields.py:436 msgid "The submitted file is empty." msgstr "El archivo enviado está vacío." -#: forms/fields.py:438 +#: forms/fields.py:437 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -4788,7 +4796,7 @@ msgstr "" "Asegúrese de que este nombre de archivo tenga como máximo %(max)d caracteres " "(tiene %(length)d)." -#: forms/fields.py:473 +#: forms/fields.py:472 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -4796,14 +4804,14 @@ msgstr "" "Seleccione una imagen válida. El archivo que ha seleccionado no es una " "imagen o es un un archivo de imagen corrupto." -#: forms/fields.py:596 forms/fields.py:671 +#: forms/fields.py:595 forms/fields.py:670 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Seleccione una opción válida. %(value)s no es una de las opciones " "disponibles." -#: forms/fields.py:672 forms/fields.py:734 forms/models.py:1002 +#: forms/fields.py:671 forms/fields.py:733 forms/models.py:1002 msgid "Enter a list of values." msgstr "Introduzca una lista de valores." @@ -4871,18 +4879,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/es_AR/LC_MESSAGES/djangojs.mo b/django/conf/locale/es_AR/LC_MESSAGES/djangojs.mo index f894397efb65..db805c50efbf 100644 Binary files a/django/conf/locale/es_AR/LC_MESSAGES/djangojs.mo and b/django/conf/locale/es_AR/LC_MESSAGES/djangojs.mo differ diff --git a/django/conf/locale/es_AR/LC_MESSAGES/djangojs.po b/django/conf/locale/es_AR/LC_MESSAGES/djangojs.po index 5f6278533a8b..ddb54e4d1743 100644 --- a/django/conf/locale/es_AR/LC_MESSAGES/djangojs.po +++ b/django/conf/locale/es_AR/LC_MESSAGES/djangojs.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-04 22:01-0300\n" -"PO-Revision-Date: 2010-05-04 22:10-0300\n" +"POT-Creation-Date: 2010-08-06 16:09-0300\n" +"PO-Revision-Date: 2010-08-06 15:59-0300\n" "Last-Translator: Ramiro Morales \n" "Language-Team: Django-I18N \n" "Language: es_AR\n" @@ -17,45 +17,17 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Pootle 2.0.1\n" -#: contrib/admin/media/js/SelectFilter2.js:37 -#, perl-format -msgid "Available %s" -msgstr "%s disponibles" - -#: contrib/admin/media/js/SelectFilter2.js:45 -msgid "Choose all" -msgstr "Seleccionar todos" - -#: contrib/admin/media/js/SelectFilter2.js:50 -msgid "Add" -msgstr "Agregar" - -#: contrib/admin/media/js/SelectFilter2.js:52 -msgid "Remove" -msgstr "Eliminar" - -#: contrib/admin/media/js/SelectFilter2.js:57 -#, perl-format -msgid "Chosen %s" -msgstr "%s elegidos" - -#: contrib/admin/media/js/SelectFilter2.js:58 -msgid "Select your choice(s) and click " -msgstr "Seleccione los items a agregar y haga click en " - #: contrib/admin/media/js/SelectFilter2.js:63 msgid "Clear all" msgstr "Eliminar todos" #: contrib/admin/media/js/actions.js:18 -#: contrib/admin/media/js/actions.min.js:1 msgid "%(sel)s of %(cnt)s selected" msgid_plural "%(sel)s of %(cnt)s selected" msgstr[0] "%(sel)s de %(cnt)s seleccionado/a" msgstr[1] "%(sel)s de %(cnt)s seleccionados/as" #: contrib/admin/media/js/actions.js:109 -#: contrib/admin/media/js/actions.min.js:5 msgid "" "You have unsaved changes on individual editable fields. If you run an " "action, your unsaved changes will be lost." diff --git a/django/conf/locale/et/LC_MESSAGES/django.mo b/django/conf/locale/et/LC_MESSAGES/django.mo index 426ee9c9371b..92203eecde76 100644 Binary files a/django/conf/locale/et/LC_MESSAGES/django.mo and b/django/conf/locale/et/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/et/LC_MESSAGES/django.po b/django/conf/locale/et/LC_MESSAGES/django.po index 9b8ae9e27a6d..f5430ef835ac 100644 --- a/django/conf/locale/et/LC_MESSAGES/django.po +++ b/django/conf/locale/et/LC_MESSAGES/django.po @@ -4111,18 +4111,18 @@ msgstr[1] "%(size)d baiti" #: template/defaultfilters.py:800 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:802 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:803 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/et/formats.py b/django/conf/locale/et/formats.py index b96420c0c1d2..101fceed1ff5 100644 --- a/django/conf/locale/et/formats.py +++ b/django/conf/locale/et/formats.py @@ -14,5 +14,5 @@ # TIME_INPUT_FORMATS = # DATETIME_INPUT_FORMATS = DECIMAL_SEPARATOR = ',' -THOUSAND_SEPARATOR = ' ' +THOUSAND_SEPARATOR = u' ' # Non-breaking space # NUMBER_GROUPING = diff --git a/django/conf/locale/eu/LC_MESSAGES/django.mo b/django/conf/locale/eu/LC_MESSAGES/django.mo index c19c32116ff6..fc4b49a4de77 100644 Binary files a/django/conf/locale/eu/LC_MESSAGES/django.mo and b/django/conf/locale/eu/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/eu/LC_MESSAGES/django.po b/django/conf/locale/eu/LC_MESSAGES/django.po index 37a829d1ca1f..8af5fb4090f5 100644 --- a/django/conf/locale/eu/LC_MESSAGES/django.po +++ b/django/conf/locale/eu/LC_MESSAGES/django.po @@ -3940,18 +3940,18 @@ msgstr[1] "" #: template/defaultfilters.py:724 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:726 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:727 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/fa/LC_MESSAGES/django.po b/django/conf/locale/fa/LC_MESSAGES/django.po index 2536b7404d06..f2e46458eb0b 100644 --- a/django/conf/locale/fa/LC_MESSAGES/django.po +++ b/django/conf/locale/fa/LC_MESSAGES/django.po @@ -3709,17 +3709,17 @@ msgstr[0] "%(size)d بایت" #: template/defaultfilters.py:739 #, python-format -msgid "%.1f KB" +msgid "%s KB" msgstr "" #: template/defaultfilters.py:741 #, python-format -msgid "%.1f MB" +msgid "%s MB" msgstr "" #: template/defaultfilters.py:742 #, python-format -msgid "%.1f GB" +msgid "%s GB" msgstr "" #: utils/dateformat.py:41 diff --git a/django/conf/locale/fi/LC_MESSAGES/django.mo b/django/conf/locale/fi/LC_MESSAGES/django.mo index 316cdbb38df0..5803bc3a3da2 100644 Binary files a/django/conf/locale/fi/LC_MESSAGES/django.mo and b/django/conf/locale/fi/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/fi/LC_MESSAGES/django.po b/django/conf/locale/fi/LC_MESSAGES/django.po index ef87894a7766..e5d5d3c2343a 100644 --- a/django/conf/locale/fi/LC_MESSAGES/django.po +++ b/django/conf/locale/fi/LC_MESSAGES/django.po @@ -4811,18 +4811,18 @@ msgstr[1] "%(size)d tavua" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f Kt" +msgid "%s KB" +msgstr "%s Kt" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f Mt" +msgid "%s MB" +msgstr "%s Mt" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f Gt" +msgid "%s GB" +msgstr "%s Gt" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/fi/formats.py b/django/conf/locale/fi/formats.py index 670e26841a76..d6cc7d76d947 100644 --- a/django/conf/locale/fi/formats.py +++ b/django/conf/locale/fi/formats.py @@ -14,5 +14,5 @@ # TIME_INPUT_FORMATS = # DATETIME_INPUT_FORMATS = DECIMAL_SEPARATOR = ',' -THOUSAND_SEPARATOR = ' ' +THOUSAND_SEPARATOR = u' ' # Non-breaking space # NUMBER_GROUPING = diff --git a/django/conf/locale/fr/LC_MESSAGES/django.mo b/django/conf/locale/fr/LC_MESSAGES/django.mo index eae7521b8000..926dcae50e9f 100644 Binary files a/django/conf/locale/fr/LC_MESSAGES/django.mo and b/django/conf/locale/fr/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/fr/LC_MESSAGES/django.po b/django/conf/locale/fr/LC_MESSAGES/django.po index 412c2cb53559..d241c77662cd 100644 --- a/django/conf/locale/fr/LC_MESSAGES/django.po +++ b/django/conf/locale/fr/LC_MESSAGES/django.po @@ -10,9 +10,9 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-14 11:01+0200\n" -"PO-Revision-Date: 2010-04-17 00:18+0200\n" -"Last-Translator: David Larlet \n" +"POT-Creation-Date: 2010-08-09 12:11+0200\n" +"PO-Revision-Date: 2010-08-09 14:38+0200\n" +"Last-Translator: Stéphane Raimbault \n" "Language-Team: French \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -72,7 +72,7 @@ msgid "Spanish" msgstr "Espagnol" #: conf/global_settings.py:57 -msgid "Argentinean Spanish" +msgid "Argentinian Spanish" msgstr "Espagnol argentin" #: conf/global_settings.py:58 @@ -168,98 +168,102 @@ msgid "Macedonian" msgstr "Macédonien" #: conf/global_settings.py:81 +msgid "Malayalam" +msgstr "Malayâlam" + +#: conf/global_settings.py:82 msgid "Mongolian" msgstr "Mongole" -#: conf/global_settings.py:82 +#: conf/global_settings.py:83 msgid "Dutch" msgstr "Hollandais" -#: conf/global_settings.py:83 +#: conf/global_settings.py:84 msgid "Norwegian" msgstr "Norvégien" -#: conf/global_settings.py:84 +#: conf/global_settings.py:85 msgid "Norwegian Bokmal" msgstr "Norvégien Bokmal" -#: conf/global_settings.py:85 +#: conf/global_settings.py:86 msgid "Norwegian Nynorsk" msgstr "Norvégien Nynorsk" -#: conf/global_settings.py:86 +#: conf/global_settings.py:87 msgid "Polish" msgstr "Polonais" -#: conf/global_settings.py:87 +#: conf/global_settings.py:88 msgid "Portuguese" msgstr "Portugais" -#: conf/global_settings.py:88 +#: conf/global_settings.py:89 msgid "Brazilian Portuguese" msgstr "Portugais brésilien" -#: conf/global_settings.py:89 +#: conf/global_settings.py:90 msgid "Romanian" msgstr "Roumain" -#: conf/global_settings.py:90 +#: conf/global_settings.py:91 msgid "Russian" msgstr "Russe" -#: conf/global_settings.py:91 +#: conf/global_settings.py:92 msgid "Slovak" msgstr "Slovaque" -#: conf/global_settings.py:92 +#: conf/global_settings.py:93 msgid "Slovenian" msgstr "Slovène" -#: conf/global_settings.py:93 +#: conf/global_settings.py:94 msgid "Albanian" msgstr "Albanais" -#: conf/global_settings.py:94 +#: conf/global_settings.py:95 msgid "Serbian" msgstr "Serbe" -#: conf/global_settings.py:95 +#: conf/global_settings.py:96 msgid "Serbian Latin" msgstr "Serbe latin" -#: conf/global_settings.py:96 +#: conf/global_settings.py:97 msgid "Swedish" msgstr "Suédois" -#: conf/global_settings.py:97 +#: conf/global_settings.py:98 msgid "Tamil" msgstr "Tamoul" -#: conf/global_settings.py:98 +#: conf/global_settings.py:99 msgid "Telugu" msgstr "Télougou" -#: conf/global_settings.py:99 +#: conf/global_settings.py:100 msgid "Thai" msgstr "Thaï" -#: conf/global_settings.py:100 +#: conf/global_settings.py:101 msgid "Turkish" msgstr "Turc" -#: conf/global_settings.py:101 +#: conf/global_settings.py:102 msgid "Ukrainian" msgstr "Ukrainien" -#: conf/global_settings.py:102 +#: conf/global_settings.py:103 msgid "Vietnamese" msgstr "Vietnamien" -#: conf/global_settings.py:103 +#: conf/global_settings.py:104 msgid "Simplified Chinese" msgstr "Chinois simplifié" -#: conf/global_settings.py:104 +#: conf/global_settings.py:105 msgid "Traditional Chinese" msgstr "Chinois traditionnel" @@ -311,15 +315,15 @@ msgstr "Ce mois-ci" msgid "This year" msgstr "Cette année" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "Yes" msgstr "Oui" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "No" msgstr "Non" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:478 msgid "Unknown" msgstr "Inconnu" @@ -697,7 +701,7 @@ msgid "Filter" msgstr "Filtre" #: contrib/admin/templates/admin/delete_confirmation.html:10 -#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:302 +#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:300 msgid "Delete" msgstr "Supprimer" @@ -796,7 +800,7 @@ msgstr "" #: contrib/admin/templates/admin/login.html:19 msgid "Username:" -msgstr "Nom d'utilisateur :" +msgstr "Nom d'utilisateur :" #: contrib/admin/templates/admin/login.html:22 msgid "Password:" @@ -859,7 +863,7 @@ msgstr "Enregistrer et ajouter un nouveau" msgid "Save and continue editing" msgstr "Enregistrer et continuer les modifications" -#: contrib/admin/templates/admin/auth/user/add_form.html:5 +#: contrib/admin/templates/admin/auth/user/add_form.html:6 msgid "" "First, enter a username and password. Then, you'll be able to edit more user " "options." @@ -867,6 +871,10 @@ msgstr "" "Saisissez tout d'abord un nom d'utilisateur et un mot de passe. Vous pourrez " "ensuite modifier plus d'options." +#: contrib/admin/templates/admin/auth/user/add_form.html:8 +msgid "Enter a username and password." +msgstr "Saisissez un nom d'utilisateur et un mot de passe." + #: contrib/admin/templates/admin/auth/user/change_password.html:28 #, python-format msgid "Enter a new password for the user %(username)s." @@ -1441,8 +1449,8 @@ msgstr "message" msgid "Logged out" msgstr "Déconnecté" -#: contrib/auth/management/commands/createsuperuser.py:23 -#: core/validators.py:120 forms/fields.py:428 +#: contrib/auth/management/commands/createsuperuser.py:24 +#: core/validators.py:120 forms/fields.py:427 msgid "Enter a valid e-mail address." msgstr "Entrez une adresse de courriel valide." @@ -1511,7 +1519,7 @@ msgid "Email address" msgstr "Adresse électronique" #: contrib/comments/forms.py:95 contrib/flatpages/admin.py:8 -#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1101 +#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1112 msgid "URL" msgstr "URL" @@ -1563,7 +1571,7 @@ msgstr "commentaire" msgid "date/time submitted" msgstr "date et heure soumises" -#: contrib/comments/models.py:60 db/models/fields/__init__.py:896 +#: contrib/comments/models.py:60 db/models/fields/__init__.py:907 msgid "IP address" msgstr "adresse IP" @@ -4513,26 +4521,26 @@ msgstr "sites" msgid "Enter a valid value." msgstr "Saisissez une valeur valide." -#: core/validators.py:87 forms/fields.py:529 +#: core/validators.py:87 forms/fields.py:528 msgid "Enter a valid URL." msgstr "Saisissez une URL valide." -#: core/validators.py:89 forms/fields.py:530 +#: core/validators.py:89 forms/fields.py:529 msgid "This URL appears to be a broken link." msgstr "Cette URL semble être cassée." -#: core/validators.py:123 forms/fields.py:873 +#: core/validators.py:123 forms/fields.py:877 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Ce champ ne doit contenir que des lettres, des nombres, des tirets bas _ et " "des traits d'union." -#: core/validators.py:126 forms/fields.py:866 +#: core/validators.py:126 forms/fields.py:870 msgid "Enter a valid IPv4 address." msgstr "Saisissez une adresse IPv4 valide." -#: core/validators.py:129 db/models/fields/__init__.py:572 +#: core/validators.py:129 db/models/fields/__init__.py:575 msgid "Enter only digits separated by commas." msgstr "Saisissez uniquement des chiffres séparés par des virgules." @@ -4543,13 +4551,13 @@ msgstr "" "Assurez-vous que cette valeur est %(limit_value)s (actuellement %(show_value)" "s)." -#: core/validators.py:153 forms/fields.py:205 forms/fields.py:257 +#: core/validators.py:153 forms/fields.py:204 forms/fields.py:256 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "" "Assurez-vous que cette valeur est inférieure ou égale à %(limit_value)s." -#: core/validators.py:158 forms/fields.py:206 forms/fields.py:258 +#: core/validators.py:158 forms/fields.py:205 forms/fields.py:257 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "" @@ -4602,134 +4610,134 @@ msgstr "Ce champ ne peut pas être vide." msgid "Field of type: %(field_type)s" msgstr "Champ de type : %(field_type)s" -#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:852 -#: db/models/fields/__init__.py:961 db/models/fields/__init__.py:972 -#: db/models/fields/__init__.py:999 +#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:863 +#: db/models/fields/__init__.py:972 db/models/fields/__init__.py:983 +#: db/models/fields/__init__.py:1010 msgid "Integer" msgstr "Entier" -#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:850 +#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:861 msgid "This value must be an integer." msgstr "Cette valeur doit être un entier." -#: db/models/fields/__init__.py:490 +#: db/models/fields/__init__.py:493 msgid "This value must be either True or False." msgstr "Cette valeur doit être soit vraie (True) soit fausse (False)." -#: db/models/fields/__init__.py:492 +#: db/models/fields/__init__.py:495 msgid "Boolean (Either True or False)" msgstr "Booléen (soit vrai ou faux)" -#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:982 +#: db/models/fields/__init__.py:542 db/models/fields/__init__.py:993 #, python-format msgid "String (up to %(max_length)s)" msgstr "Chaîne de caractère (jusqu'à %(max_length)s)" -#: db/models/fields/__init__.py:567 +#: db/models/fields/__init__.py:570 msgid "Comma-separated integers" msgstr "Des entiers séparés par une virgule" -#: db/models/fields/__init__.py:581 +#: db/models/fields/__init__.py:584 msgid "Date (without time)" msgstr "Date (sans l'heure)" -#: db/models/fields/__init__.py:585 +#: db/models/fields/__init__.py:588 msgid "Enter a valid date in YYYY-MM-DD format." msgstr "Saisissez une date valide au format AAAA-MM-JJ." -#: db/models/fields/__init__.py:586 +#: db/models/fields/__init__.py:589 #, python-format msgid "Invalid date: %s" msgstr "Date non valide : %s" -#: db/models/fields/__init__.py:667 +#: db/models/fields/__init__.py:670 msgid "Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format." msgstr "" "Saisissez une date et une heure valides au format AAAA-MM-JJ HH:MM[:ss[." "uuuuuu]]." -#: db/models/fields/__init__.py:669 +#: db/models/fields/__init__.py:672 msgid "Date (with time)" msgstr "Date (avec l'heure)" -#: db/models/fields/__init__.py:735 +#: db/models/fields/__init__.py:738 msgid "This value must be a decimal number." msgstr "Cette valeur doit être un nombre décimal." -#: db/models/fields/__init__.py:737 +#: db/models/fields/__init__.py:740 msgid "Decimal number" msgstr "Nombre décimal" -#: db/models/fields/__init__.py:792 +#: db/models/fields/__init__.py:795 msgid "E-mail address" msgstr "Adresse électronique" -#: db/models/fields/__init__.py:799 db/models/fields/files.py:220 +#: db/models/fields/__init__.py:810 db/models/fields/files.py:220 #: db/models/fields/files.py:331 msgid "File path" msgstr "Chemin vers le fichier" -#: db/models/fields/__init__.py:822 +#: db/models/fields/__init__.py:833 msgid "This value must be a float." msgstr "Cette valeur doit être un nombre réel." -#: db/models/fields/__init__.py:824 +#: db/models/fields/__init__.py:835 msgid "Floating point number" msgstr "Nombre à virgule flottante" -#: db/models/fields/__init__.py:883 +#: db/models/fields/__init__.py:894 msgid "Big (8 byte) integer" msgstr "Grand entier (8 octets)" -#: db/models/fields/__init__.py:912 +#: db/models/fields/__init__.py:923 msgid "This value must be either None, True or False." msgstr "Cette valeur doit être nulle (None), vraie (True) ou fausse (False)." -#: db/models/fields/__init__.py:914 +#: db/models/fields/__init__.py:925 msgid "Boolean (Either True, False or None)" msgstr "Booléen (soit vrai, faux ou nul)" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1016 msgid "Text" msgstr "Texte" -#: db/models/fields/__init__.py:1021 +#: db/models/fields/__init__.py:1032 msgid "Time" msgstr "Heure" -#: db/models/fields/__init__.py:1025 +#: db/models/fields/__init__.py:1036 msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." msgstr "Saisissez une heure valide au format HH:MM[:ss[.uuuuuu]]." -#: db/models/fields/__init__.py:1109 +#: db/models/fields/__init__.py:1128 msgid "XML text" msgstr "Texte XML" -#: db/models/fields/related.py:799 +#: db/models/fields/related.py:801 #, python-format msgid "Model %(model)s with pk %(pk)r does not exist." msgstr "Le modèle %(model)s avec la clef primaire %(pk)r n'existe pas." -#: db/models/fields/related.py:801 +#: db/models/fields/related.py:803 msgid "Foreign Key (type determined by related field)" msgstr "Clé étrangère (type défini par le champ lié)" -#: db/models/fields/related.py:918 +#: db/models/fields/related.py:921 msgid "One-to-one relationship" msgstr "Relation un à un" -#: db/models/fields/related.py:980 +#: db/models/fields/related.py:983 msgid "Many-to-many relationship" msgstr "Relation plusieurs à plusieurs" -#: db/models/fields/related.py:1000 +#: db/models/fields/related.py:1003 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." msgstr "" "Maintenez appuyé « Ctrl », ou « Commande (touche pomme) » sur un Mac, pour en " "sélectionner plusieurs." -#: db/models/fields/related.py:1061 +#: db/models/fields/related.py:1064 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." msgid_plural "" @@ -4743,55 +4751,55 @@ msgstr[1] "" msgid "This field is required." msgstr "Ce champ est obligatoire." -#: forms/fields.py:204 +#: forms/fields.py:203 msgid "Enter a whole number." msgstr "Saisissez un nombre entier." -#: forms/fields.py:235 forms/fields.py:256 +#: forms/fields.py:234 forms/fields.py:255 msgid "Enter a number." msgstr "Saisissez un nombre." -#: forms/fields.py:259 +#: forms/fields.py:258 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Assurez-vous qu'il n'y a pas plus de %s chiffres au total." -#: forms/fields.py:260 +#: forms/fields.py:259 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Assurez-vous qu'il n'y a pas plus de %s chiffres après la virgule." -#: forms/fields.py:261 +#: forms/fields.py:260 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Assurez-vous qu'il n'y a pas plus de %s chiffres avant la virgule." -#: forms/fields.py:323 forms/fields.py:838 +#: forms/fields.py:322 forms/fields.py:837 msgid "Enter a valid date." msgstr "Saisissez une date valide." -#: forms/fields.py:351 forms/fields.py:839 +#: forms/fields.py:350 forms/fields.py:838 msgid "Enter a valid time." msgstr "Saisissez une heure valide." -#: forms/fields.py:377 +#: forms/fields.py:376 msgid "Enter a valid date/time." msgstr "Saisissez une date et une heure valides." -#: forms/fields.py:435 +#: forms/fields.py:434 msgid "No file was submitted. Check the encoding type on the form." msgstr "" "Aucun fichier n'a été soumis. Vérifiez le type d'encodage du formulaire." -#: forms/fields.py:436 +#: forms/fields.py:435 msgid "No file was submitted." msgstr "Aucun fichier n'a été soumis." -#: forms/fields.py:437 +#: forms/fields.py:436 msgid "The submitted file is empty." msgstr "Le fichier soumis est vide." -#: forms/fields.py:438 +#: forms/fields.py:437 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." @@ -4799,7 +4807,7 @@ msgstr "" "Assurez-vous que ce nom de fichier ne contient pas plus de %(max)d " "caractères (actuellement %(length)d caractères)." -#: forms/fields.py:473 +#: forms/fields.py:472 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -4807,17 +4815,17 @@ msgstr "" "Téléversez une image valide. Le fichier que vous avez transféré n'est pas " "une image ou bien est corrompu." -#: forms/fields.py:596 forms/fields.py:671 +#: forms/fields.py:595 forms/fields.py:670 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Sélectionnez un choix valide. %(value)s n'en fait pas partie." -#: forms/fields.py:672 forms/fields.py:734 forms/models.py:1002 +#: forms/fields.py:671 forms/fields.py:733 forms/models.py:1002 msgid "Enter a list of values." msgstr "Saisissez une liste de valeurs." # Si « : » est requis, créer un ticket -#: forms/formsets.py:298 forms/formsets.py:300 +#: forms/formsets.py:296 forms/formsets.py:298 msgid "Order" msgstr "Ordre" @@ -4868,31 +4876,31 @@ msgstr "Sélectionnez un choix valide ; %s n'en fait pas partie." msgid "\"%s\" is not a valid value for a primary key." msgstr "« %s » n'est pas une valeur correcte pour une clé primaire." -#: template/defaultfilters.py:776 +#: template/defaultfilters.py:780 msgid "yes,no,maybe" msgstr "oui, non, peut-être" -#: template/defaultfilters.py:807 +#: template/defaultfilters.py:811 #, python-format msgid "%(size)d byte" msgid_plural "%(size)d bytes" msgstr[0] "%(size)d octet" msgstr[1] "%(size)d octets" -#: template/defaultfilters.py:809 +#: template/defaultfilters.py:813 #, python-format -msgid "%.1f KB" -msgstr "%.1f Ko" +msgid "%s KB" +msgstr "%s Ko" -#: template/defaultfilters.py:811 +#: template/defaultfilters.py:815 #, python-format -msgid "%.1f MB" -msgstr "%.1f Mo" +msgid "%s MB" +msgstr "%s Mo" -#: template/defaultfilters.py:812 +#: template/defaultfilters.py:816 #, python-format -msgid "%.1f GB" -msgstr "%.1f Go" +msgid "%s GB" +msgstr "%s Go" #: utils/dateformat.py:42 msgid "p.m." @@ -5100,7 +5108,7 @@ msgstr "nov." msgid "Dec." msgstr "déc." -#: utils/text.py:130 +#: utils/text.py:136 msgid "or" msgstr "ou" diff --git a/django/conf/locale/fy_NL/LC_MESSAGES/django.po b/django/conf/locale/fy_NL/LC_MESSAGES/django.po index a975658146d1..049905ed7c4c 100644 --- a/django/conf/locale/fy_NL/LC_MESSAGES/django.po +++ b/django/conf/locale/fy_NL/LC_MESSAGES/django.po @@ -106,17 +106,17 @@ msgstr[1] "" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f GB" +msgid "%s GB" msgstr "" #: template/defaultfilters.py:808 #, python-format -msgid "%.1f KB" +msgid "%s KB" msgstr "" #: template/defaultfilters.py:810 #, python-format -msgid "%.1f MB" +msgid "%s MB" msgstr "" #: contrib/admin/sites.py:447 diff --git a/django/conf/locale/ga/LC_MESSAGES/django.mo b/django/conf/locale/ga/LC_MESSAGES/django.mo index 5f41030ee6da..91a19f784edb 100644 Binary files a/django/conf/locale/ga/LC_MESSAGES/django.mo and b/django/conf/locale/ga/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ga/LC_MESSAGES/django.po b/django/conf/locale/ga/LC_MESSAGES/django.po index 913583c6f86c..ac3631c0fa8e 100644 --- a/django/conf/locale/ga/LC_MESSAGES/django.po +++ b/django/conf/locale/ga/LC_MESSAGES/django.po @@ -4727,18 +4727,18 @@ msgstr[4] "%(size)d bearta" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/gl/LC_MESSAGES/django.mo b/django/conf/locale/gl/LC_MESSAGES/django.mo index 491d161b5f3f..000caf0de559 100644 Binary files a/django/conf/locale/gl/LC_MESSAGES/django.mo and b/django/conf/locale/gl/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/gl/LC_MESSAGES/django.po b/django/conf/locale/gl/LC_MESSAGES/django.po index f5db1f052da2..0562c39314e0 100644 --- a/django/conf/locale/gl/LC_MESSAGES/django.po +++ b/django/conf/locale/gl/LC_MESSAGES/django.po @@ -2728,18 +2728,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:522 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:524 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:525 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:40 msgid "p.m." diff --git a/django/conf/locale/he/LC_MESSAGES/django.mo b/django/conf/locale/he/LC_MESSAGES/django.mo index 4d7ffe01c396..84e4593fec75 100644 Binary files a/django/conf/locale/he/LC_MESSAGES/django.mo and b/django/conf/locale/he/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/he/LC_MESSAGES/django.po b/django/conf/locale/he/LC_MESSAGES/django.po index 50ae4f87fa29..176c5ba92490 100644 --- a/django/conf/locale/he/LC_MESSAGES/django.po +++ b/django/conf/locale/he/LC_MESSAGES/django.po @@ -4757,18 +4757,18 @@ msgstr[1] "%(size)d בתים" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/hi/LC_MESSAGES/django.mo b/django/conf/locale/hi/LC_MESSAGES/django.mo index 99f31869eec8..4febdb951341 100644 Binary files a/django/conf/locale/hi/LC_MESSAGES/django.mo and b/django/conf/locale/hi/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/hi/LC_MESSAGES/django.po b/django/conf/locale/hi/LC_MESSAGES/django.po index 65877bdaf09c..348db7f72787 100644 --- a/django/conf/locale/hi/LC_MESSAGES/django.po +++ b/django/conf/locale/hi/LC_MESSAGES/django.po @@ -3818,18 +3818,18 @@ msgstr[1] "%(size)d बाइट" #: template/defaultfilters.py:774 #, python-format -msgid "%.1f KB" -msgstr "%.1f के.बी" +msgid "%s KB" +msgstr "%s के.बी" #: template/defaultfilters.py:776 #, python-format -msgid "%.1f MB" -msgstr "%.1f एम.बी" +msgid "%s MB" +msgstr "%s एम.बी" #: template/defaultfilters.py:777 #, python-format -msgid "%.1f GB" -msgstr "%.1f जी.बी" +msgid "%s GB" +msgstr "%s जी.बी" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/hr/LC_MESSAGES/django.mo b/django/conf/locale/hr/LC_MESSAGES/django.mo index cb50e47f0275..c737a39df599 100644 Binary files a/django/conf/locale/hr/LC_MESSAGES/django.mo and b/django/conf/locale/hr/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/hr/LC_MESSAGES/django.po b/django/conf/locale/hr/LC_MESSAGES/django.po index a9020044ff34..dac0f17d1aa9 100644 --- a/django/conf/locale/hr/LC_MESSAGES/django.po +++ b/django/conf/locale/hr/LC_MESSAGES/django.po @@ -4865,18 +4865,18 @@ msgstr[3] "%(size)d byte-a" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/hu/LC_MESSAGES/django.mo b/django/conf/locale/hu/LC_MESSAGES/django.mo index 2d351ee7601f..3ed2641ed944 100644 Binary files a/django/conf/locale/hu/LC_MESSAGES/django.mo and b/django/conf/locale/hu/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/hu/LC_MESSAGES/django.po b/django/conf/locale/hu/LC_MESSAGES/django.po index c4cb1a062223..c7513a7e9bc5 100644 --- a/django/conf/locale/hu/LC_MESSAGES/django.po +++ b/django/conf/locale/hu/LC_MESSAGES/django.po @@ -4119,18 +4119,18 @@ msgstr[1] "%(size)d bájt" #: template/defaultfilters.py:739 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:741 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:742 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/hu/formats.py b/django/conf/locale/hu/formats.py index 6ee2db0cd570..cb93e2f91d17 100644 --- a/django/conf/locale/hu/formats.py +++ b/django/conf/locale/hu/formats.py @@ -14,5 +14,5 @@ # TIME_INPUT_FORMATS = # DATETIME_INPUT_FORMATS = DECIMAL_SEPARATOR = ',' -THOUSAND_SEPARATOR = ' ' +THOUSAND_SEPARATOR = u' ' # Non-breaking space # NUMBER_GROUPING = diff --git a/django/conf/locale/id/LC_MESSAGES/django.mo b/django/conf/locale/id/LC_MESSAGES/django.mo index b015ca033b71..49d65f87c74f 100644 Binary files a/django/conf/locale/id/LC_MESSAGES/django.mo and b/django/conf/locale/id/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/id/LC_MESSAGES/django.po b/django/conf/locale/id/LC_MESSAGES/django.po index 67b89e9a72a5..6ea48118f188 100644 --- a/django/conf/locale/id/LC_MESSAGES/django.po +++ b/django/conf/locale/id/LC_MESSAGES/django.po @@ -4820,18 +4820,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/is/LC_MESSAGES/django.mo b/django/conf/locale/is/LC_MESSAGES/django.mo index 18e701093c3c..8b7f802f8200 100644 Binary files a/django/conf/locale/is/LC_MESSAGES/django.mo and b/django/conf/locale/is/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/is/LC_MESSAGES/django.po b/django/conf/locale/is/LC_MESSAGES/django.po index c7472a9acbe9..2ccff7b4654b 100644 --- a/django/conf/locale/is/LC_MESSAGES/django.po +++ b/django/conf/locale/is/LC_MESSAGES/django.po @@ -3641,18 +3641,18 @@ msgstr[1] "%(size)d bæti" #: template/defaultfilters.py:739 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:741 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:742 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/it/LC_MESSAGES/django.mo b/django/conf/locale/it/LC_MESSAGES/django.mo index ee440eb018a4..e19810cdbcf5 100644 Binary files a/django/conf/locale/it/LC_MESSAGES/django.mo and b/django/conf/locale/it/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/it/LC_MESSAGES/django.po b/django/conf/locale/it/LC_MESSAGES/django.po index 5e608d4b1f22..3627f09fd3e9 100644 --- a/django/conf/locale/it/LC_MESSAGES/django.po +++ b/django/conf/locale/it/LC_MESSAGES/django.po @@ -4816,18 +4816,18 @@ msgstr[1] "%(size)d byte" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/ja/LC_MESSAGES/django.mo b/django/conf/locale/ja/LC_MESSAGES/django.mo index 1428c39dbf68..ef3326a7084b 100644 Binary files a/django/conf/locale/ja/LC_MESSAGES/django.mo and b/django/conf/locale/ja/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ja/LC_MESSAGES/django.po b/django/conf/locale/ja/LC_MESSAGES/django.po index 83e8e122087e..b899e72c3265 100644 --- a/django/conf/locale/ja/LC_MESSAGES/django.po +++ b/django/conf/locale/ja/LC_MESSAGES/django.po @@ -4772,18 +4772,18 @@ msgstr[0] "%(size)d バイト" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/ka/LC_MESSAGES/django.mo b/django/conf/locale/ka/LC_MESSAGES/django.mo index 997a164477f5..f06c4bb72bf4 100644 Binary files a/django/conf/locale/ka/LC_MESSAGES/django.mo and b/django/conf/locale/ka/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ka/LC_MESSAGES/django.po b/django/conf/locale/ka/LC_MESSAGES/django.po index 205ad3fd468e..7c4ff8c2fbb3 100644 --- a/django/conf/locale/ka/LC_MESSAGES/django.po +++ b/django/conf/locale/ka/LC_MESSAGES/django.po @@ -3872,18 +3872,18 @@ msgstr[1] "%(size)d ბაიტი" #: template/defaultfilters.py:747 #, python-format -msgid "%.1f KB" -msgstr "%.1f კბაიტი" +msgid "%s KB" +msgstr "%s კბაიტი" #: template/defaultfilters.py:749 #, python-format -msgid "%.1f MB" -msgstr "%.1f მბაიტი" +msgid "%s MB" +msgstr "%s მბაიტი" #: template/defaultfilters.py:750 #, python-format -msgid "%.1f GB" -msgstr "%.1f გბაიტი" +msgid "%s GB" +msgstr "%s გბაიტი" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/ko/LC_MESSAGES/django.mo b/django/conf/locale/ko/LC_MESSAGES/django.mo index 4609c145f49a..8d2ad12a9faa 100644 Binary files a/django/conf/locale/ko/LC_MESSAGES/django.mo and b/django/conf/locale/ko/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ko/LC_MESSAGES/django.po b/django/conf/locale/ko/LC_MESSAGES/django.po index 04f3f274bbbf..f4bc0f4d72ee 100644 --- a/django/conf/locale/ko/LC_MESSAGES/django.po +++ b/django/conf/locale/ko/LC_MESSAGES/django.po @@ -4778,18 +4778,18 @@ msgstr[1] "%(size)d 바이트" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/lt/LC_MESSAGES/django.mo b/django/conf/locale/lt/LC_MESSAGES/django.mo index 3d428274b1d7..576dab4fd16d 100644 Binary files a/django/conf/locale/lt/LC_MESSAGES/django.mo and b/django/conf/locale/lt/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/lt/LC_MESSAGES/django.po b/django/conf/locale/lt/LC_MESSAGES/django.po index d855d87ddb1c..5edc6837a85e 100644 --- a/django/conf/locale/lt/LC_MESSAGES/django.po +++ b/django/conf/locale/lt/LC_MESSAGES/django.po @@ -2853,16 +2853,16 @@ msgstr[1] "%(size)d baitai" #: template/defaultfilters.py:522 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:524 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:525 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" diff --git a/django/conf/locale/lv/LC_MESSAGES/django.mo b/django/conf/locale/lv/LC_MESSAGES/django.mo index ee2c0b34f854..8d27fcfdf115 100644 Binary files a/django/conf/locale/lv/LC_MESSAGES/django.mo and b/django/conf/locale/lv/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/lv/LC_MESSAGES/django.po b/django/conf/locale/lv/LC_MESSAGES/django.po index 79ca8a4b9f00..7eb4a5514de9 100644 --- a/django/conf/locale/lv/LC_MESSAGES/django.po +++ b/django/conf/locale/lv/LC_MESSAGES/django.po @@ -4830,18 +4830,18 @@ msgstr[2] "%(size)d baitu" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/lv/formats.py b/django/conf/locale/lv/formats.py index 8328b743208f..15637d1dbd1d 100644 --- a/django/conf/locale/lv/formats.py +++ b/django/conf/locale/lv/formats.py @@ -32,5 +32,5 @@ '%d.%m.%y', # '25.10.06' ) DECIMAL_SEPARATOR = ',' -THOUSAND_SEPARATOR = ' ' +THOUSAND_SEPARATOR = u' ' # Non-breaking space NUMBER_GROUPING = 3 diff --git a/django/conf/locale/mk/LC_MESSAGES/django.mo b/django/conf/locale/mk/LC_MESSAGES/django.mo index 19d924aa0fc6..a55e8d41cf2b 100644 Binary files a/django/conf/locale/mk/LC_MESSAGES/django.mo and b/django/conf/locale/mk/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/mk/LC_MESSAGES/django.po b/django/conf/locale/mk/LC_MESSAGES/django.po index 22b7a377b839..6c8b2eb215fe 100644 --- a/django/conf/locale/mk/LC_MESSAGES/django.po +++ b/django/conf/locale/mk/LC_MESSAGES/django.po @@ -4833,18 +4833,18 @@ msgstr[1] "%(size)d бајти" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f КБ" +msgid "%s KB" +msgstr "%s КБ" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f МБ" +msgid "%s MB" +msgstr "%s МБ" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f ГБ" +msgid "%s GB" +msgstr "%s ГБ" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/ml/LC_MESSAGES/django.mo b/django/conf/locale/ml/LC_MESSAGES/django.mo new file mode 100644 index 000000000000..654b4fc4ee2b Binary files /dev/null and b/django/conf/locale/ml/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ml/LC_MESSAGES/django.po b/django/conf/locale/ml/LC_MESSAGES/django.po new file mode 100644 index 000000000000..c46824e6a8b6 --- /dev/null +++ b/django/conf/locale/ml/LC_MESSAGES/django.po @@ -0,0 +1,5044 @@ +# Translation of Django to Malayalam. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# RAJEESH R NAIR , 2010. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Django SVN\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-05-27 11:01+0530\n" +"PO-Revision-Date: 2010-05-28 15:09+0530\n" +"Last-Translator: Rajeesh Nair \n" +"Language-Team: MALAYALAM \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Language: Malayalam\n" +"X-Poedit-Country: INDIA\n" + +#: conf/global_settings.py:44 +msgid "Arabic" +msgstr "അറബി" + +#: conf/global_settings.py:45 +msgid "Bulgarian" +msgstr "ബള്‍ഗേറിയന്‍" + +#: conf/global_settings.py:46 +msgid "Bengali" +msgstr "ബംഗാളി" + +#: conf/global_settings.py:47 +msgid "Bosnian" +msgstr "ബോസ്നിയന്‍" + +#: conf/global_settings.py:48 +msgid "Catalan" +msgstr "കാറ്റലന്‍" + +#: conf/global_settings.py:49 +msgid "Czech" +msgstr "ചെക്" + +#: conf/global_settings.py:50 +msgid "Welsh" +msgstr "വെല്‍ഷ്" + +#: conf/global_settings.py:51 +msgid "Danish" +msgstr "ഡാനിഷ്" + +#: conf/global_settings.py:52 +msgid "German" +msgstr "ജര്‍മന്‍" + +#: conf/global_settings.py:53 +msgid "Greek" +msgstr "ഗ്രീക്ക്" + +#: conf/global_settings.py:54 +msgid "English" +msgstr "ഇംഗ്ളീഷ്" + +#: conf/global_settings.py:55 +msgid "British English" +msgstr "ബ്രിട്ടീഷ് ഇംഗ്ളീഷ്" + +#: conf/global_settings.py:56 +msgid "Spanish" +msgstr "സ്പാനിഷ്" + +#: conf/global_settings.py:57 +msgid "Argentinean Spanish" +msgstr "അര്‍ജന്റീനിയന്‍ സ്പാനിഷ്" + +#: conf/global_settings.py:58 +msgid "Estonian" +msgstr "എസ്ടോണിയന്‍ സ്പാനിഷ്" + +#: conf/global_settings.py:59 +msgid "Basque" +msgstr "ബാസ്ക്യു" + +#: conf/global_settings.py:60 +msgid "Persian" +msgstr "പേര്‍ഷ്യന്‍" + +#: conf/global_settings.py:61 +msgid "Finnish" +msgstr "ഫിന്നിഷ്" + +#: conf/global_settings.py:62 +msgid "French" +msgstr "ഫ്രെഞ്ച്" + +#: conf/global_settings.py:63 +msgid "Frisian" +msgstr "ഫ്രിസിയന്‍" + +#: conf/global_settings.py:64 +msgid "Irish" +msgstr "ഐറിഷ്" + +#: conf/global_settings.py:65 +msgid "Galician" +msgstr "ഗലിഷ്യന്‍" + +#: conf/global_settings.py:66 +msgid "Hebrew" +msgstr "ഹീബ്റു" + +#: conf/global_settings.py:67 +msgid "Hindi" +msgstr "ഹിന്ദി" + +#: conf/global_settings.py:68 +msgid "Croatian" +msgstr "ക്രൊയേഷ്യന്‍" + +#: conf/global_settings.py:69 +msgid "Hungarian" +msgstr "ഹംഗേറിയന്‍" + +#: conf/global_settings.py:70 +msgid "Indonesian" +msgstr "ഇന്‍ദൊനേഷ്യന്‍" + +#: conf/global_settings.py:71 +msgid "Icelandic" +msgstr "ഐസ്ലാന്‍ഡിക്" + +#: conf/global_settings.py:72 +msgid "Italian" +msgstr "ഇറ്റാലിയന്‍" + +#: conf/global_settings.py:73 +msgid "Japanese" +msgstr "ജാപ്പനീസ്" + +#: conf/global_settings.py:74 +msgid "Georgian" +msgstr "ജോര്‍ജിയന്‍" + +#: conf/global_settings.py:75 +msgid "Khmer" +msgstr "" + +#: conf/global_settings.py:76 +msgid "Kannada" +msgstr "കന്നഡ" + +#: conf/global_settings.py:77 +msgid "Korean" +msgstr "കൊറിയന്‍" + +#: conf/global_settings.py:78 +msgid "Lithuanian" +msgstr "ലിത്വാനിയന്‍" + +#: conf/global_settings.py:79 +msgid "Latvian" +msgstr "ലാറ്റ്വിയന്‍" + +#: conf/global_settings.py:80 +msgid "Macedonian" +msgstr "മാസിഡോണിയന്‍" + +#: conf/global_settings.py:81 +msgid "Mongolian" +msgstr "മംഗോളിയന്‍" + +#: conf/global_settings.py:82 +msgid "Dutch" +msgstr "ഡച്ച്" + +#: conf/global_settings.py:83 +msgid "Norwegian" +msgstr "" + +#: conf/global_settings.py:84 +msgid "Norwegian Bokmal" +msgstr "" + +#: conf/global_settings.py:85 +msgid "Norwegian Nynorsk" +msgstr "" + +#: conf/global_settings.py:86 +msgid "Polish" +msgstr "" + +#: conf/global_settings.py:87 +msgid "Portuguese" +msgstr "" + +#: conf/global_settings.py:88 +msgid "Brazilian Portuguese" +msgstr "" + +#: conf/global_settings.py:89 +msgid "Romanian" +msgstr "" + +#: conf/global_settings.py:90 +msgid "Russian" +msgstr "" + +#: conf/global_settings.py:91 +msgid "Slovak" +msgstr "" + +#: conf/global_settings.py:92 +msgid "Slovenian" +msgstr "" + +#: conf/global_settings.py:93 +msgid "Albanian" +msgstr "" + +#: conf/global_settings.py:94 +msgid "Serbian" +msgstr "" + +#: conf/global_settings.py:95 +msgid "Serbian Latin" +msgstr "" + +#: conf/global_settings.py:96 +msgid "Swedish" +msgstr "" + +#: conf/global_settings.py:97 +msgid "Tamil" +msgstr "" + +#: conf/global_settings.py:98 +msgid "Telugu" +msgstr "" + +#: conf/global_settings.py:99 +msgid "Thai" +msgstr "" + +#: conf/global_settings.py:100 +msgid "Turkish" +msgstr "" + +#: conf/global_settings.py:101 +msgid "Ukrainian" +msgstr "" + +#: conf/global_settings.py:102 +msgid "Vietnamese" +msgstr "" + +#: conf/global_settings.py:103 +msgid "Simplified Chinese" +msgstr "" + +#: conf/global_settings.py:104 +msgid "Traditional Chinese" +msgstr "" + +#: contrib/admin/actions.py:48 +#, python-format +msgid "Successfully deleted %(count)d %(items)s." +msgstr "%(count)d %(items)s വിജയകരമായി ഡിലീറ്റ് ചെയ്തു." + +#: contrib/admin/actions.py:55 contrib/admin/options.py:1125 +msgid "Are you sure?" +msgstr "തീര്‍ച്ചയാണോ?" + +#: contrib/admin/actions.py:73 +#, python-format +msgid "Delete selected %(verbose_name_plural)s" +msgstr "തെരഞ്ഞെടുത്ത %(verbose_name_plural)s ഡിലീറ്റ് ചെയ്യുക." + +#: contrib/admin/filterspecs.py:44 +#, python-format +msgid "" +"

By %s:

\n" +"
    \n" +msgstr "" +"

    By %s:

    \n" +"
      \n" + +#: contrib/admin/filterspecs.py:75 contrib/admin/filterspecs.py:92 +#: contrib/admin/filterspecs.py:147 contrib/admin/filterspecs.py:173 +msgid "All" +msgstr "എല്ലാം" + +#: contrib/admin/filterspecs.py:113 +msgid "Any date" +msgstr "ഏതെങ്കിലും തീയതി" + +#: contrib/admin/filterspecs.py:114 +msgid "Today" +msgstr "ഇന്ന്" + +#: contrib/admin/filterspecs.py:117 +msgid "Past 7 days" +msgstr "കഴിഞ്ഞ ഏഴു ദിവസം" + +#: contrib/admin/filterspecs.py:119 +msgid "This month" +msgstr "ഈ മാസം" + +#: contrib/admin/filterspecs.py:121 +msgid "This year" +msgstr "ഈ വര്‍ഷം" + +#: contrib/admin/filterspecs.py:147 forms/widgets.py:466 +msgid "Yes" +msgstr "അതെ" + +#: contrib/admin/filterspecs.py:147 forms/widgets.py:466 +msgid "No" +msgstr "അല്ല" + +#: contrib/admin/filterspecs.py:154 forms/widgets.py:466 +msgid "Unknown" +msgstr "അജ്ഞാതം" + +#: contrib/admin/helpers.py:20 +msgid "Action:" +msgstr "ആക്ഷന്‍" + +#: contrib/admin/models.py:19 +msgid "action time" +msgstr "ആക്ഷന്‍ സമയം" + +#: contrib/admin/models.py:22 +msgid "object id" +msgstr "ഒബ്ജെക്ട് ഐഡി" + +#: contrib/admin/models.py:23 +msgid "object repr" +msgstr "ഒബ്ജെക്ട് സൂചന" + +#: contrib/admin/models.py:24 +msgid "action flag" +msgstr "ആക്ഷന്‍ ഫ്ളാഗ്" + +#: contrib/admin/models.py:25 +msgid "change message" +msgstr "സന്ദേശം മാറ്റുക" + +#: contrib/admin/models.py:28 +msgid "log entry" +msgstr "ലോഗ് എന്ട്രി" + +#: contrib/admin/models.py:29 +msgid "log entries" +msgstr "ലോഗ് എന്ട്രികള്‍" + +#: contrib/admin/options.py:138 contrib/admin/options.py:153 +msgid "None" +msgstr "ഒന്നുമില്ല" + +#: contrib/admin/options.py:559 +#, python-format +msgid "Changed %s." +msgstr "%s മാറ്റി." + +#: contrib/admin/options.py:559 contrib/admin/options.py:569 +#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:845 +#: forms/models.py:568 +msgid "and" +msgstr "ഉം" + +#: contrib/admin/options.py:564 +#, python-format +msgid "Added %(name)s \"%(object)s\"." +msgstr "%(name)s \"%(object)s\" ചേര്‍ത്തു." + +#: contrib/admin/options.py:568 +#, python-format +msgid "Changed %(list)s for %(name)s \"%(object)s\"." +msgstr "%(name)s \"%(object)s\" ന്റെ %(list)s മാറ്റി." + +#: contrib/admin/options.py:573 +#, python-format +msgid "Deleted %(name)s \"%(object)s\"." +msgstr "%(name)s \"%(object)s\" ഡിലീറ്റ് ചെയ്തു." + +#: contrib/admin/options.py:577 +msgid "No fields changed." +msgstr "ഒരു മാറ്റവുമില്ല." + +#: contrib/admin/options.py:643 +#, python-format +msgid "The %(name)s \"%(obj)s\" was added successfully." +msgstr "%(name)s \"%(obj)s\" വിജയകരമായി കൂട്ടിച്ചേര്ത്തു." + +#: contrib/admin/options.py:647 contrib/admin/options.py:680 +msgid "You may edit it again below." +msgstr "താഴെ നിന്ന് വീണ്ടും മാറ്റം വരുത്താം" + +#: contrib/admin/options.py:657 contrib/admin/options.py:690 +#, python-format +msgid "You may add another %s below." +msgstr "%s ഒന്നു കൂടി ചേര്‍ക്കാം" + +#: contrib/admin/options.py:678 +#, python-format +msgid "The %(name)s \"%(obj)s\" was changed successfully." +msgstr "%(name)s \"%(obj)s\" ല്‍ മാറ്റം വരുത്തി." + +#: contrib/admin/options.py:686 +#, python-format +msgid "" +"The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." +msgstr "%(name)s \"%(obj)s\" കൂട്ടി ചേര്‍ത്തു. താഴെ നിന്നും മാറ്റം വരുത്താം." + +#: contrib/admin/options.py:740 contrib/admin/options.py:997 +msgid "" +"Items must be selected in order to perform actions on them. No items have " +"been changed." +msgstr "ആക്ഷന്‍ നടപ്പിലാക്കേണ്ട വകകള്‍ തെരഞ്ഞെടുക്കണം. ഒന്നും മാറ്റിയിട്ടില്ല." + +#: contrib/admin/options.py:759 +msgid "No action selected." +msgstr "ആക്ഷനൊന്നും തെരഞ്ഞെടുത്തില്ല." + +#: contrib/admin/options.py:840 +#, python-format +msgid "Add %s" +msgstr "%s ചേര്‍ക്കുക" + +#: contrib/admin/options.py:866 contrib/admin/options.py:1105 +#, python-format +msgid "%(name)s object with primary key %(key)r does not exist." +msgstr "%(key)r എന്ന പ്രാഥമിക കീ ഉള്ള %(name)s വസ്തു ഒന്നും നിലവിലില്ല." + +#: contrib/admin/options.py:931 +#, python-format +msgid "Change %s" +msgstr "%s മാറ്റാം" + +#: contrib/admin/options.py:977 +msgid "Database error" +msgstr "ഡേറ്റാബേസ് തകരാറാണ്." + +#: contrib/admin/options.py:1039 +#, python-format +msgid "%(count)s %(name)s was changed successfully." +msgid_plural "%(count)s %(name)s were changed successfully." +msgstr[0] "%(count)s %(name)s ല്‍ മാറ്റം വരുത്തി." +msgstr[1] "%(count)s %(name)s ല്‍ മാറ്റം വരുത്തി." + +#: contrib/admin/options.py:1066 +#, python-format +msgid "%(total_count)s selected" +msgid_plural "All %(total_count)s selected" +msgstr[0] "%(total_count)s തെരഞ്ഞെടുത്തു." +msgstr[1] "%(total_count)sഉം തെരഞ്ഞെടുത്തു." + +#: contrib/admin/options.py:1071 +#, python-format +msgid "0 of %(cnt)s selected" +msgstr "%(cnt)s ല്‍ ഒന്നും തെരഞ്ഞെടുത്തില്ല." + +#: contrib/admin/options.py:1118 +#, python-format +msgid "The %(name)s \"%(obj)s\" was deleted successfully." +msgstr "%(name)s \"%(obj)s\" ഡിലീറ്റ് ചെയ്തു." + +#: contrib/admin/options.py:1155 +#, python-format +msgid "Change history: %s" +msgstr "%s ലെ മാറ്റങ്ങള്‍." + +#: contrib/admin/sites.py:18 contrib/admin/views/decorators.py:14 +#: contrib/auth/forms.py:81 +msgid "" +"Please enter a correct username and password. Note that both fields are case-" +"sensitive." +msgstr "ദയവായി ശരിയായ യൂസര്‍നാമവും പാസ്വേര്ഡും നല്കുക. ഇംഗ്ളീഷ് അക്ഷരങ്ങള്‍ വല്യക്ഷരമാണോ അല്ലയോ എന്നത് " +"ശ്രദ്ധിക്കണം" + +#: contrib/admin/sites.py:307 contrib/admin/views/decorators.py:40 +msgid "Please log in again, because your session has expired." +msgstr "താങ്കളുടെ സെഷന്റെ കാലാവധി കഴിഞ്ഞു. വീണ്ടും ലോഗിന്‍ ചെയ്യണം." + +#: contrib/admin/sites.py:314 contrib/admin/views/decorators.py:47 +msgid "" +"Looks like your browser isn't configured to accept cookies. Please enable " +"cookies, reload this page, and try again." +msgstr "നിങ്ങളുടെ ബ്രൗസര്‍ കുക്കീസ് സ്വീകരിക്കാന്‍ തയ്യാറല്ലെന്നു തോന്നുന്നു. കുക്കീസ് പ്രവര്‍ത്തനക്ഷമമാക്കിയ ശേഷം " +"ഈ പേജ് രീലോഡ് ചെയ്ത് വീണ്ടും ശ്രമിക്കുക." + +#: contrib/admin/sites.py:330 contrib/admin/sites.py:336 +#: contrib/admin/views/decorators.py:66 +msgid "Usernames cannot contain the '@' character." +msgstr "യൂസര്‍നാമത്തില്‍ '@' എന്ന ചിഹ്നം പാടില്ല." + +#: contrib/admin/sites.py:333 contrib/admin/views/decorators.py:62 +#, python-format +msgid "Your e-mail address is not your username. Try '%s' instead." +msgstr "നിങ്ങളുടെ ഇ-മെയില്‍ അഡ്രസ്സ് അല്ല യൂസര്‍നാമം. പകരം '%s' ഉപയോഗിച്ച് നോക്കുക." + +#: contrib/admin/sites.py:389 +msgid "Site administration" +msgstr "സൈറ്റ് ഭരണം" + +#: contrib/admin/sites.py:403 contrib/admin/templates/admin/login.html:26 +#: contrib/admin/templates/registration/password_reset_complete.html:14 +#: contrib/admin/views/decorators.py:20 +msgid "Log in" +msgstr "ലോഗ്-ഇന്‍" + +#: contrib/admin/sites.py:448 +#, python-format +msgid "%s administration" +msgstr "%s ഭരണം" + +#: contrib/admin/widgets.py:75 +msgid "Date:" +msgstr "തീയതി:" + +#: contrib/admin/widgets.py:75 +msgid "Time:" +msgstr "സമയം:" + +#: contrib/admin/widgets.py:99 +msgid "Currently:" +msgstr "" + +#: contrib/admin/widgets.py:99 +msgid "Change:" +msgstr "മാറ്റുക:" + +#: contrib/admin/widgets.py:129 +msgid "Lookup" +msgstr "തിരയുക" + +#: contrib/admin/widgets.py:244 +msgid "Add Another" +msgstr "ഒന്നു കൂടി ചേര്‍ക്കുക" + +#: contrib/admin/templates/admin/404.html:4 +#: contrib/admin/templates/admin/404.html:8 +msgid "Page not found" +msgstr "പേജ് കണ്ടില്ല" + +#: contrib/admin/templates/admin/404.html:10 +msgid "We're sorry, but the requested page could not be found." +msgstr "ക്ഷമിക്കണം, ആവശ്യപ്പെട്ട പേജ് കണ്ടെത്താന്‍ കഴിഞ്ഞില്ല." + +#: contrib/admin/templates/admin/500.html:4 +#: contrib/admin/templates/admin/app_index.html:8 +#: contrib/admin/templates/admin/base.html:55 +#: contrib/admin/templates/admin/change_form.html:18 +#: contrib/admin/templates/admin/change_list.html:42 +#: contrib/admin/templates/admin/delete_confirmation.html:6 +#: contrib/admin/templates/admin/delete_selected_confirmation.html:6 +#: contrib/admin/templates/admin/invalid_setup.html:4 +#: contrib/admin/templates/admin/object_history.html:6 +#: contrib/admin/templates/admin/auth/user/change_password.html:11 +#: contrib/admin/templates/registration/logged_out.html:4 +#: contrib/admin/templates/registration/password_change_done.html:4 +#: contrib/admin/templates/registration/password_change_form.html:5 +#: contrib/admin/templates/registration/password_reset_complete.html:4 +#: contrib/admin/templates/registration/password_reset_confirm.html:4 +#: contrib/admin/templates/registration/password_reset_done.html:4 +#: contrib/admin/templates/registration/password_reset_form.html:4 +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:3 +msgid "Home" +msgstr "പൂമുഖം" + +#: contrib/admin/templates/admin/500.html:4 +msgid "Server error" +msgstr "സെര്‍വര്‍ തകരാറാണ്" + +#: contrib/admin/templates/admin/500.html:6 +msgid "Server error (500)" +msgstr "സെര്‍വര്‍ തകരാറാണ് (500)" + +#: contrib/admin/templates/admin/500.html:9 +msgid "Server Error (500)" +msgstr "സെര്‍വര്‍ തകരാറാണ് (500)" + +#: contrib/admin/templates/admin/500.html:10 +msgid "" +"There's been an error. It's been reported to the site administrators via e-" +"mail and should be fixed shortly. Thanks for your patience." +msgstr "എന്തോ തകരാറുണ്ട്. ഉടന്‍ പരിഹരിക്കാനായി സൈറ്റ് നിയന്ത്രകര്‍ക്കു ഇ-മെയില്‍ വഴി രിപ്പോര്‍ട്ട് ചെയ്തിട്ടുണ്ട്." +"ദയവായി കാത്തിരിക്കുക." + +#: contrib/admin/templates/admin/actions.html:4 +msgid "Run the selected action" +msgstr "തെരഞ്ഞെടുത്ത ആക്ഷന്‍ നടപ്പിലാക്കുക" + +#: contrib/admin/templates/admin/actions.html:4 +msgid "Go" +msgstr "" + +#: contrib/admin/templates/admin/actions.html:11 +msgid "Click here to select the objects across all pages" +msgstr "എല്ലാ പേജിലേയും വസ്തുക്കള്‍ തെരഞ്ഞെടുക്കാന്‍ ഇവിടെ ക്ലിക് ചെയ്യുക." + +#: contrib/admin/templates/admin/actions.html:11 +#, python-format +msgid "Select all %(total_count)s %(module_name)s" +msgstr "മുഴുവന്‍ %(total_count)s %(module_name)s ഉം തെരഞ്ഞെടുക്കുക" + +#: contrib/admin/templates/admin/actions.html:13 +msgid "Clear selection" +msgstr "തെരഞ്ഞെടുത്തത് റദ്ദാക്കുക." + +#: contrib/admin/templates/admin/app_index.html:10 +#: contrib/admin/templates/admin/index.html:19 +#, python-format +msgid "%(name)s" +msgstr "" + +#: contrib/admin/templates/admin/base.html:28 +msgid "Welcome," +msgstr "സ്വാഗതം, " + +#: contrib/admin/templates/admin/base.html:33 +#: contrib/admin/templates/registration/password_change_done.html:3 +#: contrib/admin/templates/registration/password_change_form.html:4 +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:3 +msgid "Documentation" +msgstr "സഹായക്കുറിപ്പുകള്‍" + +#: contrib/admin/templates/admin/base.html:41 +#: contrib/admin/templates/admin/auth/user/change_password.html:15 +#: contrib/admin/templates/admin/auth/user/change_password.html:48 +#: contrib/admin/templates/registration/password_change_done.html:3 +#: contrib/admin/templates/registration/password_change_form.html:4 +msgid "Change password" +msgstr "പാസ് വേര്‍ഡ് മാറ്റുക." + +#: contrib/admin/templates/admin/base.html:48 +#: contrib/admin/templates/registration/password_change_done.html:3 +#: contrib/admin/templates/registration/password_change_form.html:4 +msgid "Log out" +msgstr "പുറത്ത് കടക്കുക." + +#: contrib/admin/templates/admin/base_site.html:4 +msgid "Django site admin" +msgstr "ജാംഗോ സൈറ്റ് അഡ്മിന്‍" + +#: contrib/admin/templates/admin/base_site.html:7 +msgid "Django administration" +msgstr "ജാംഗോ ഭരണം" + +#: contrib/admin/templates/admin/change_form.html:21 +#: contrib/admin/templates/admin/index.html:29 +msgid "Add" +msgstr "" + +#: contrib/admin/templates/admin/change_form.html:28 +#: contrib/admin/templates/admin/object_history.html:10 +msgid "History" +msgstr "ചരിത്രം" + +#: contrib/admin/templates/admin/change_form.html:29 +#: contrib/admin/templates/admin/edit_inline/stacked.html:9 +#: contrib/admin/templates/admin/edit_inline/tabular.html:28 +msgid "View on site" +msgstr "" + +#: contrib/admin/templates/admin/change_form.html:39 +#: contrib/admin/templates/admin/change_list.html:71 +#: contrib/admin/templates/admin/auth/user/change_password.html:24 +#: contrib/admin/templates/registration/password_change_form.html:15 +msgid "Please correct the error below." +msgid_plural "Please correct the errors below." +msgstr[0] "ദയവായി താഴെയുള്ള തെറ്റ് പരിഹരിക്കുക." +msgstr[1] "ദയവായി താഴെയുള്ള തെറ്റുകള്‍ പരിഹരിക്കുക." + +#: contrib/admin/templates/admin/change_list.html:63 +#, python-format +msgid "Add %(name)s" +msgstr "" + +#: contrib/admin/templates/admin/change_list.html:82 +msgid "Filter" +msgstr "" + +#: contrib/admin/templates/admin/delete_confirmation.html:10 +#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:302 +msgid "Delete" +msgstr "" + +#: contrib/admin/templates/admin/delete_confirmation.html:16 +#, python-format +msgid "" +"Deleting the %(object_name)s '%(escaped_object)s' would result in deleting " +"related objects, but your account doesn't have permission to delete the " +"following types of objects:" +msgstr "%(object_name)s '%(escaped_object)s ഡിലീറ്റ് ചെയ്യുമ്പോള്‍ അതുമായി ബന്ധമുള്ള വസ്തുക്കളും" +"ഡിലീറ്റ് ആവും. പക്ഷേ നിങ്ങള്‍ക്ക് താഴെ പറഞ്ഞ തരം വസ്തുക്കള്‍ ഡിലീറ്റ് ചെയ്യാനുള്ള അനുമതി ഇല്ല:" + +#: contrib/admin/templates/admin/delete_confirmation.html:23 +#, python-format +msgid "" +"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " +"All of the following related items will be deleted:" +msgstr "%(object_name)s \"%(escaped_object)s\" ഡിലീറ്റ് ചെയ്യണമെന്ന് തീര്‍ച്ചയാണോ?" +"അതുമായി ബന്ധമുള്ള താഴെപ്പറയുന്ന വസ്തുക്കളെല്ലാം ഡിലീറ്റ് ആവും:" + +#: contrib/admin/templates/admin/delete_confirmation.html:28 +#: contrib/admin/templates/admin/delete_selected_confirmation.html:33 +msgid "Yes, I'm sure" +msgstr "അതെ, തീര്‍ച്ചയാണ്" + +#: contrib/admin/templates/admin/delete_selected_confirmation.html:9 +msgid "Delete multiple objects" +msgstr "ഒന്നിലേറെ വസ്തുക്കള്‍ ഡിലീറ്റ് ചെയ്തോളൂ" + +#: contrib/admin/templates/admin/delete_selected_confirmation.html:15 +#, python-format +msgid "" +"Deleting the %(object_name)s would result in deleting related objects, but " +"your account doesn't have permission to delete the following types of " +"objects:" +msgstr "%(object_name)s ഡിലീറ്റ് ചെയ്യുമ്പോള്‍ അതുമായി ബന്ധമുള്ള വസ്തുക്കളും" +"ഡിലീറ്റ് ആവും. പക്ഷേ നിങ്ങള്‍ക്ക് താഴെ പറഞ്ഞ തരം വസ്തുക്കള്‍ ഡിലീറ്റ് ചെയ്യാനുള്ള അനുമതി ഇല്ല:" + +#: contrib/admin/templates/admin/delete_selected_confirmation.html:22 +#, python-format +msgid "" +"Are you sure you want to delete the selected %(object_name)s objects? All of " +"the following objects and their related items will be deleted:" +msgstr "തെരഞ്ഞെടുത്ത %(object_name)s എല്ലാം ഡിലീറ്റ് ചെയ്യണമെന്ന് തീര്‍ച്ചയാണോ?" +"താഴെപ്പറയുന്ന വസ്തുക്കളും അതുമായി ബന്ധമുള്ളതെല്ലാം ഡിലീറ്റ് ആവും:" + +#: contrib/admin/templates/admin/filter.html:2 +#, python-format +msgid " By %(filter_title)s " +msgstr "" + +#: contrib/admin/templates/admin/index.html:18 +#, python-format +msgid "Models available in the %(name)s application." +msgstr "" + +#: contrib/admin/templates/admin/index.html:35 +msgid "Change" +msgstr "മാറ്റുക" + +#: contrib/admin/templates/admin/index.html:45 +msgid "You don't have permission to edit anything." +msgstr "ഒന്നിലും മാറ്റം വരുത്താനുള്ള അനുമതി ഇല്ല." + +#: contrib/admin/templates/admin/index.html:53 +msgid "Recent Actions" +msgstr "സമീപകാല പ്രവ്രുത്തികള്‍" + +#: contrib/admin/templates/admin/index.html:54 +msgid "My Actions" +msgstr "എന്റെ പ്രവ്രുത്തികള്‍" + +#: contrib/admin/templates/admin/index.html:58 +msgid "None available" +msgstr "ഒന്നും ലഭ്യമല്ല" + +#: contrib/admin/templates/admin/index.html:72 +msgid "Unknown content" +msgstr "ഉള്ളടക്കം അറിയില്ല." + +#: contrib/admin/templates/admin/invalid_setup.html:7 +msgid "" +"Something's wrong with your database installation. Make sure the appropriate " +"database tables have been created, and make sure the database is readable by " +"the appropriate user." +msgstr "നിങ്ങളുടെ ഡേറ്റാബേസ് ഇന്‍സ്ടാലേഷനില്‍ എന്തോ പിശകുണ്ട്. ശരിയായ ടേബിളുകള്‍ ഉണ്ടെന്നും ഡേറ്റാബേസ് " +"വായനായോഗ്യമാണെന്നും ഉറപ്പു വരുത്തുക." + +#: contrib/admin/templates/admin/login.html:19 +msgid "Username:" +msgstr "യൂസര്‍ നാമം" + +#: contrib/admin/templates/admin/login.html:22 +msgid "Password:" +msgstr "പാസ് വേര്‍ഡ്" + +#: contrib/admin/templates/admin/object_history.html:22 +msgid "Date/time" +msgstr "തീയതി/സമയം" + +#: contrib/admin/templates/admin/object_history.html:23 +msgid "User" +msgstr "യൂസര്‍" + +#: contrib/admin/templates/admin/object_history.html:24 +msgid "Action" +msgstr "ആക്ഷന്‍" + +#: contrib/admin/templates/admin/object_history.html:38 +msgid "" +"This object doesn't have a change history. It probably wasn't added via this " +"admin site." +msgstr "ഈ വസ്തുവിന്റെ മാറ്റങ്ങളുടെ ചരിത്രം ലഭ്യമല്ല. ഒരുപക്ഷെ ഇത് അഡ്മിന്‍ സൈറ്റ് വഴി ചേര്‍ത്തതായിരിക്കില്ല." + +#: contrib/admin/templates/admin/pagination.html:10 +msgid "Show all" +msgstr "എല്ലാം കാണട്ടെ" + +#: contrib/admin/templates/admin/pagination.html:11 +#: contrib/admin/templates/admin/submit_line.html:3 +msgid "Save" +msgstr "സേവ് ചെയ്യണം" + +#: contrib/admin/templates/admin/search_form.html:8 +msgid "Search" +msgstr "പരതുക" + +#: contrib/admin/templates/admin/search_form.html:10 +#, python-format +msgid "1 result" +msgid_plural "%(counter)s results" +msgstr[0] "1 ഫലം" +msgstr[1] "%(counter)s ഫലങ്ങള്‍" + +#: contrib/admin/templates/admin/search_form.html:10 +#, python-format +msgid "%(full_result_count)s total" +msgstr "ആകെ %(full_result_count)s" + +#: contrib/admin/templates/admin/submit_line.html:5 +msgid "Save as new" +msgstr "പുതിയതായി സേവ് ചെയ്യണം" + +#: contrib/admin/templates/admin/submit_line.html:6 +msgid "Save and add another" +msgstr "സേവ് ചെയ്ത ശേഷം വേറെ ചേര്‍ക്കണം" + +#: contrib/admin/templates/admin/submit_line.html:7 +msgid "Save and continue editing" +msgstr "സേവ് ചെയ്ത ശേഷം മാറ്റം വരുത്താം" + +#: contrib/admin/templates/admin/auth/user/add_form.html:5 +msgid "" +"First, enter a username and password. Then, you'll be able to edit more user " +"options." +msgstr "ആദ്യം, യൂസര്‍ നാമവും പാസ് വേര്‍ഡും നല്കണം. പിന്നെ, കൂടുതല്‍ കാര്യങ്ങള്‍ മാറ്റാവുന്നതാണ്." + +#: contrib/admin/templates/admin/auth/user/change_password.html:28 +#, python-format +msgid "Enter a new password for the user %(username)s." +msgstr "%(username)s ന് പുതിയ പാസ് വേര്‍ഡ് നല്കുക." + +#: contrib/admin/templates/admin/auth/user/change_password.html:35 +#: contrib/auth/forms.py:17 contrib/auth/forms.py:61 contrib/auth/forms.py:186 +msgid "Password" +msgstr "പാസ് വേര്‍ഡ്" + +#: contrib/admin/templates/admin/auth/user/change_password.html:41 +#: contrib/admin/templates/registration/password_change_form.html:37 +#: contrib/auth/forms.py:187 +msgid "Password (again)" +msgstr "പാസ് വേര്‍ഡ് (വീണ്ടും)" + +#: contrib/admin/templates/admin/auth/user/change_password.html:42 +#: contrib/auth/forms.py:19 +msgid "Enter the same password as above, for verification." +msgstr "പാസ് വേര്‍ഡ് മുകളിലെ പോലെ തന്നെ നല്കുക. (ഉറപ്പു വരുത്താനാണ്.)" + +#: contrib/admin/templates/admin/edit_inline/stacked.html:64 +#: contrib/admin/templates/admin/edit_inline/tabular.html:110 +#, python-format +msgid "Add another %(verbose_name)s" +msgstr "%(verbose_name)s ഒന്നു കൂടി ചേര്‍ക്കുക" + +#: contrib/admin/templates/admin/edit_inline/stacked.html:67 +#: contrib/admin/templates/admin/edit_inline/tabular.html:113 +#: contrib/comments/templates/comments/delete.html:12 +msgid "Remove" +msgstr "നീക്കം ചെയ്യുക" + +#: contrib/admin/templates/admin/edit_inline/tabular.html:15 +msgid "Delete?" +msgstr "ഡിലീറ്റ് ചെയ്യട്ടെ?" + +#: contrib/admin/templates/registration/logged_out.html:8 +msgid "Thanks for spending some quality time with the Web site today." +msgstr "ഈ വെബ് സൈറ്റില്‍ കുറെ നല്ല സമയം ചെലവഴിച്ചതിനു നന്ദി." + +#: contrib/admin/templates/registration/logged_out.html:10 +msgid "Log in again" +msgstr "വീണ്ടും ലോഗ്-ഇന്‍ ചെയ്യുക." + +#: contrib/admin/templates/registration/password_change_done.html:4 +#: contrib/admin/templates/registration/password_change_form.html:5 +#: contrib/admin/templates/registration/password_change_form.html:7 +#: contrib/admin/templates/registration/password_change_form.html:19 +msgid "Password change" +msgstr "പാസ് വേര്‍ഡ് മാറ്റം" + +#: contrib/admin/templates/registration/password_change_done.html:6 +#: contrib/admin/templates/registration/password_change_done.html:10 +msgid "Password change successful" +msgstr "പാസ് വേര്‍ഡ് മാറ്റം വിജയിച്ചു" + +#: contrib/admin/templates/registration/password_change_done.html:12 +msgid "Your password was changed." +msgstr "നിങ്ങളുടെ പാസ് വേര്‍ഡ് മാറ്റിക്കഴിഞ്ഞു." + +#: contrib/admin/templates/registration/password_change_form.html:21 +msgid "" +"Please enter your old password, for security's sake, and then enter your new " +"password twice so we can verify you typed it in correctly." +msgstr "സുരക്ഷയ്ക്കായി നിങ്ങളുടെ പഴയ പാസ് വേര്‍ഡ് നല്കുക. പിന്നെ, പുതിയ പാസ് വേര്‍ഡ് രണ്ട് തവണ നല്കുക. " +"(ടയ്പ് ചെയ്തതു ശരിയാണെന്ന് ഉറപ്പാക്കാന്‍)" + +#: contrib/admin/templates/registration/password_change_form.html:27 +#: contrib/auth/forms.py:170 +msgid "Old password" +msgstr "പഴയ പാസ് വേര്‍ഡ്" + +#: contrib/admin/templates/registration/password_change_form.html:32 +#: contrib/auth/forms.py:144 +msgid "New password" +msgstr "പുതിയ പാസ് വേര്‍ഡ്" + +#: contrib/admin/templates/registration/password_change_form.html:43 +#: contrib/admin/templates/registration/password_reset_confirm.html:21 +msgid "Change my password" +msgstr "എന്റെ പാസ് വേര്‍ഡ് മാറ്റണം" + +#: contrib/admin/templates/registration/password_reset_complete.html:4 +#: contrib/admin/templates/registration/password_reset_confirm.html:6 +#: contrib/admin/templates/registration/password_reset_done.html:4 +#: contrib/admin/templates/registration/password_reset_form.html:4 +#: contrib/admin/templates/registration/password_reset_form.html:6 +#: contrib/admin/templates/registration/password_reset_form.html:10 +msgid "Password reset" +msgstr "പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കല്‍" + +#: contrib/admin/templates/registration/password_reset_complete.html:6 +#: contrib/admin/templates/registration/password_reset_complete.html:10 +msgid "Password reset complete" +msgstr "പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കല്‍ പൂര്‍ണം" + +#: contrib/admin/templates/registration/password_reset_complete.html:12 +msgid "Your password has been set. You may go ahead and log in now." +msgstr "നിങ്ങളുടെ പാസ് വേര്‍ഡ് തയ്യാര്‍. ഇനി ലോഗ്-ഇന്‍ ചെയ്യാം." + +#: contrib/admin/templates/registration/password_reset_confirm.html:4 +msgid "Password reset confirmation" +msgstr "പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കല്‍ ഉറപ്പാക്കല്‍" + +#: contrib/admin/templates/registration/password_reset_confirm.html:12 +msgid "Enter new password" +msgstr "പുതിയ പാസ് വേര്‍ഡ് നല്കൂ" + +#: contrib/admin/templates/registration/password_reset_confirm.html:14 +msgid "" +"Please enter your new password twice so we can verify you typed it in " +"correctly." +msgstr "ദയവായി നിങ്ങളുടെ പുതിയ പാസ് വേര്‍ഡ് രണ്ടു തവണ നല്കണം. ശരിയായാണ് ടൈപ്പു ചെയ്തത് എന്നു ഉറപ്പിക്കാനാണ്." + +#: contrib/admin/templates/registration/password_reset_confirm.html:18 +msgid "New password:" +msgstr "പുതിയ പാസ് വേര്‍ഡ്:" + +#: contrib/admin/templates/registration/password_reset_confirm.html:20 +msgid "Confirm password:" +msgstr "പാസ് വേര്‍ഡ് ഉറപ്പാക്കൂ:" + +#: contrib/admin/templates/registration/password_reset_confirm.html:26 +msgid "Password reset unsuccessful" +msgstr "പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കല്‍ പരാജയം" + +#: contrib/admin/templates/registration/password_reset_confirm.html:28 +msgid "" +"The password reset link was invalid, possibly because it has already been " +"used. Please request a new password reset." +msgstr "പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കാന്‍ നല്കിയ ലിങ്ക് യോഗ്യമല്ല. ഒരു പക്ഷേ, അതു മുന്പ് തന്നെ ഉപയോഗിച്ചു " +"കഴിഞ്ഞതാവാം. പുതിയ ഒരു ലിങ്കിന് അപേക്ഷിക്കൂ." + +#: contrib/admin/templates/registration/password_reset_done.html:6 +#: contrib/admin/templates/registration/password_reset_done.html:10 +msgid "Password reset successful" +msgstr "പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കല്‍ വിജയം" + +#: contrib/admin/templates/registration/password_reset_done.html:12 +msgid "" +"We've e-mailed you instructions for setting your password to the e-mail " +"address you submitted. You should be receiving it shortly." +msgstr "നിങ്ങളുടെ പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കാനായി നിര്‍ദ്ദേശങ്ങള്‍ അടങ്ങിയ ഒരു ഈ-മെയില്‍ നിങ്ങള്‍ നല്കിയ" +"വിലാസത്തില്‍ അയച്ചിട്ടുണ്ട്. അത് നിങ്ങള്‍ക്ക് ഉടന്‍ ലഭിക്കേണ്ടതാണ്." + +#: contrib/admin/templates/registration/password_reset_email.html:2 +msgid "You're receiving this e-mail because you requested a password reset" +msgstr "നിങ്ങള്‍ പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കാന്‍ അപേക്ഷിച്ചതു കൊണ്ടാണ് ഈ ഇ-മെയില്‍ നിങ്ങള്‍ക്ക് ലഭിക്കുന്നത്." + +#: contrib/admin/templates/registration/password_reset_email.html:3 +#, python-format +msgid "for your user account at %(site_name)s" +msgstr "നിങ്ങളുടെ %(site_name)s എന്ന സൈറ്റിലെ അക്കൗണ്ടിനു വേണ്ടി." + +#: contrib/admin/templates/registration/password_reset_email.html:5 +msgid "Please go to the following page and choose a new password:" +msgstr "ദയവായി താഴെ പറയുന്ന പേജ് സന്ദര്‍ശിച്ച് പുതിയ പാസ് വേര്‍ഡ് തെരഞ്ഞെടുക്കുക:" + +#: contrib/admin/templates/registration/password_reset_email.html:9 +msgid "Your username, in case you've forgotten:" +msgstr "നിങ്ങള്‍ മറന്നെങ്കില്‍, നിങ്ങളുടെ യൂസര്‍ നാമം, :" + +#: contrib/admin/templates/registration/password_reset_email.html:11 +msgid "Thanks for using our site!" +msgstr "ഞങ്ങളുടെ സൈറ്റ് ഉപയോഗിച്ചതിന് നന്ദി!" + +#: contrib/admin/templates/registration/password_reset_email.html:13 +#, python-format +msgid "The %(site_name)s team" +msgstr "" + +#: contrib/admin/templates/registration/password_reset_form.html:12 +msgid "" +"Forgotten your password? Enter your e-mail address below, and we'll e-mail " +"instructions for setting a new one." +msgstr "പാസ് വേര്‍ഡ് മറന്നോ? നിങ്ങളുടെ ഇ-മെയില്‍ വിലാസം നല്കൂ, പുതിയ പാസ് വേര്‍ഡ് സ്ഥാപിക്കാനായി " +"ഞങ്ങള്‍ നിര്‍ദ്ദേശങ്ങള്‍ ആ വിലാസത്തില്‍ അയക്കാം." + +#: contrib/admin/templates/registration/password_reset_form.html:16 +msgid "E-mail address:" +msgstr "ഇ-മെയില്‍ വിലാസം:" + +#: contrib/admin/templates/registration/password_reset_form.html:16 +msgid "Reset my password" +msgstr "എന്റെ പാസ് വേര്‍ഡ് പുനസ്ഥാപിക്കൂ" + +#: contrib/admin/templatetags/admin_list.py:257 +msgid "All dates" +msgstr "എല്ലാ തീയതികളും" + +#: contrib/admin/views/main.py:65 +#, python-format +msgid "Select %s" +msgstr "%s തെരഞ്ഞെടുക്കൂ" + +#: contrib/admin/views/main.py:65 +#, python-format +msgid "Select %s to change" +msgstr "മാറ്റാനുള്ള %s തെരഞ്ഞെടുക്കൂ" + +#: contrib/admin/views/template.py:38 contrib/sites/models.py:38 +msgid "site" +msgstr "സൈറ്റ്" + +#: contrib/admin/views/template.py:40 +msgid "template" +msgstr "ടെമ്പ്ലേറ്റ്" + +#: contrib/admindocs/views.py:61 contrib/admindocs/views.py:63 +#: contrib/admindocs/views.py:65 +msgid "tag:" +msgstr "ടാഗ്:" + +#: contrib/admindocs/views.py:94 contrib/admindocs/views.py:96 +#: contrib/admindocs/views.py:98 +msgid "filter:" +msgstr "അരിപ്പ:" + +#: contrib/admindocs/views.py:158 contrib/admindocs/views.py:160 +#: contrib/admindocs/views.py:162 +msgid "view:" +msgstr "വ്യൂ" + +#: contrib/admindocs/views.py:190 +#, python-format +msgid "App %r not found" +msgstr "%r എന്ന App കണ്ടില്ല." + +#: contrib/admindocs/views.py:197 +#, python-format +msgid "Model %(model_name)r not found in app %(app_label)r" +msgstr "%(app_label)r എന്ന Appല്‍ %(model_name)r എന്ന മാത്രുക കണ്ടില്ല." + +#: contrib/admindocs/views.py:209 +#, python-format +msgid "the related `%(app_label)s.%(data_type)s` object" +msgstr "ബന്ധപ്പെട്ട `%(app_label)s.%(data_type)s` വസ്തു" + +#: contrib/admindocs/views.py:209 contrib/admindocs/views.py:228 +#: contrib/admindocs/views.py:233 contrib/admindocs/views.py:247 +#: contrib/admindocs/views.py:261 contrib/admindocs/views.py:266 +msgid "model:" +msgstr "മാത്രുക:" + +#: contrib/admindocs/views.py:224 contrib/admindocs/views.py:256 +#, python-format +msgid "related `%(app_label)s.%(object_name)s` objects" +msgstr "ബന്ധപ്പെട്ട `%(app_label)s.%(object_name)s` വസ്തുക്കള്‍" + +#: contrib/admindocs/views.py:228 contrib/admindocs/views.py:261 +#, python-format +msgid "all %s" +msgstr "%s എല്ലാം" + +#: contrib/admindocs/views.py:233 contrib/admindocs/views.py:266 +#, python-format +msgid "number of %s" +msgstr "%sന്റെ എണ്ണം" + +#: contrib/admindocs/views.py:271 +#, python-format +msgid "Fields on %s objects" +msgstr "%s വസ്തുക്കളിലെ വിവരങ്ങള്‍" + +#: contrib/admindocs/views.py:361 +#, python-format +msgid "%s does not appear to be a urlpattern object" +msgstr "%s വിലാസ മാത്രുക (urlpattern object) ആണെന്ന് തോന്നുന്നില്ല." + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:3 +msgid "Bookmarklets" +msgstr "ബുക്ക് മാര്‍ക്കുകള്‍" + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:4 +msgid "Documentation bookmarklets" +msgstr "സഹായക്കുറിപ്പുകളുടെ ബുക്ക്മാര്‍ക്കുകള്‍" + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:8 +msgid "" +"\n" +"

      To install bookmarklets, drag the link to your bookmarks\n" +"toolbar, or right-click the link and add it to your bookmarks. Now you can\n" +"select the bookmarklet from any page in the site. Note that some of these\n" +"bookmarklets require you to be viewing the site from a computer designated\n" +"as \"internal\" (talk to your system administrator if you aren't sure if\n" +"your computer is \"internal\").

      \n" +msgstr "" +"\n" +"

      ബുക്ക്മാര്‍ക്ക്ലെറ്റുകള്‍ ഇന്‍സ്റ്റാള്‍ ചെയ്യാന്‍, ലിങ്കിനെ നിങ്ങളുടെ ബുക്ക്മാര്‍ക് ടൂള്‍ബാറിലേക്ക് \n" +"വലിച്ചിടുകയോ, ലിങ്കിന്‍മേല്‍ റൈറ്റ്ക്ളിക് ചെയ്ത് ബുക്ക്മാര്‍ക്കായി ചേര്‍ക്കുകയോ ചെയ്യുക. ഇനി സൈറ്റിലെ ഏതു പേജില്‍ നിന്നും\n" +" ഈ ബുക്ക്മാര്ക് തെരഞ്ഞെടുക്കാം. ചില ബുക്ക്മാര്‍ക്കുകള്‍ ഇന്റേണല്‍ ആയ കമ്പ്യൂട്ടറില്‍ നിന്നേ ലഭ്യമാവൂ എന്നു ശ്രദ്ധിക്കണം.\n" +"നിങ്ങളുടെ കംപ്യൂട്ടര്‍ അത്തരത്തില്‍ പെട്ടതാണോ എന്നറിയാന്‍ സിസ്റ്റം അഡ്മിനിസ്ട്രേട്ടറെ ബന്ധപ്പെടുക.

      \n" + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:18 +msgid "Documentation for this page" +msgstr "ഈ പേജിന്റെ സഹായക്കുറിപ്പുകള്‍" + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:19 +msgid "" +"Jumps you from any page to the documentation for the view that generates " +"that page." +msgstr "ഏതു പേജില്‍ നിന്നും അതിന്റെ ഉദ്ഭവമായ വ്യൂവിന്റെ സഹായക്കുറിപ്പിലേക്കു ചാടാന്‍" + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:21 +msgid "Show object ID" +msgstr "വസ്തുവിന്റെ ഐഡി കാണിക്കുക." + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:22 +msgid "" +"Shows the content-type and unique ID for pages that represent a single " +"object." +msgstr "ഒറ്റ വസ്തുവിനെ പ്രതിനിധീകരിക്കുന്ന പേജുകളുടെ ഉള്ളടക്കത്തിന്റെ തരവും തനതായ IDയും കാണിക്കുന്നു." + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:24 +msgid "Edit this object (current window)" +msgstr "ഈ വസ്തുവില് മാറ്റം വരുത്തുക (ഇതേ വിന്‍ഡോ)" + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:25 +msgid "Jumps to the admin page for pages that represent a single object." +msgstr "ഒറ്റ വസ്തുവിനെ പ്രതിനിധീകരിക്കുന്ന പേജുകള്‍ക്കുള്ള അഡ്മിന്‍ പേജിലേക്ക് ചാടുന്നു." + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:27 +msgid "Edit this object (new window)" +msgstr "ഈ വസ്തുവില് മാറ്റം വരുത്തുക (പുതിയ വിന്‍ഡോ)" + +#: contrib/admindocs/templates/admin_doc/bookmarklets.html:28 +msgid "As above, but opens the admin page in a new window." +msgstr "മുകളിലേതു പോലെ, പക്ഷെ, അഡ്മിന്‍ പേജ് പുതിയ വിന്ഡോവിലാണ് തുറക്കുക." + +#: contrib/auth/admin.py:29 +msgid "Personal info" +msgstr "വ്യക്തിപരമായ വിവരങ്ങള്‍" + +#: contrib/auth/admin.py:30 +msgid "Permissions" +msgstr "അനുമതികള്‍" + +#: contrib/auth/admin.py:31 +msgid "Important dates" +msgstr "പ്രധാന തീയതികള്‍" + +#: contrib/auth/admin.py:32 +msgid "Groups" +msgstr "ഗ്രൂപ്പുകള്‍" + +#: contrib/auth/admin.py:114 +msgid "Password changed successfully." +msgstr "പാസ് വേര്‍ഡ് മാറ്റിയിരിക്കുന്നു." + +#: contrib/auth/admin.py:124 +#, python-format +msgid "Change password: %s" +msgstr "പാസ് വേര്‍ഡ് മാറ്റുക: %s" + +#: contrib/auth/forms.py:14 contrib/auth/forms.py:48 contrib/auth/forms.py:60 +msgid "Username" +msgstr "യൂസര്‍ നാമം (ഉപയോക്ത്രു നാമം)" + +#: contrib/auth/forms.py:15 contrib/auth/forms.py:49 +msgid "Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only." +msgstr "നിര്‍ബന്ധം. 30 ഓ അതില്‍ കുറവോ ചിഹ്നങ്ങള്‍. അക്ഷരങ്ങള്‍, അക്കങ്ങള്‍, " +"പിന്നെ @/./+/-/_എന്നിവയും മാത്രം." + +#: contrib/auth/forms.py:16 contrib/auth/forms.py:50 +msgid "This value may contain only letters, numbers and @/./+/-/_ characters." +msgstr "അക്ഷരങ്ങള്‍, അക്കങ്ങള്‍, പിന്നെ @/./+/-/_എന്നിവയും മാത്രം." + +#: contrib/auth/forms.py:18 +msgid "Password confirmation" +msgstr "പാസ് വേര്‍ഡ് ഉറപ്പാക്കല്‍" + +#: contrib/auth/forms.py:31 +msgid "A user with that username already exists." +msgstr "ആ പേരുള്ള ഒരു ഉപയോക്താവ് നിലവിലുണ്ട്." + +#: contrib/auth/forms.py:37 contrib/auth/forms.py:156 +#: contrib/auth/forms.py:198 +msgid "The two password fields didn't match." +msgstr "പാസ് വേര്‍ഡ് നല്കിയ കള്ളികള്‍ രണ്ടും തമ്മില്‍ സാമ്യമില്ല." + +#: contrib/auth/forms.py:83 +msgid "This account is inactive." +msgstr "ഈ അക്കൗണ്ട് മരവിപ്പിച്ചതാണ്." + +#: contrib/auth/forms.py:88 +msgid "" +"Your Web browser doesn't appear to have cookies enabled. Cookies are " +"required for logging in." +msgstr "നിങ്ങളുടെ വെബ്-ബ്രൗസറിലെ കുക്കീസൊന്നും പ്രവര്‍ത്തിക്കുന്നില്ല. ഇതിലേക്ക് പ്രവേശിക്കാന്‍ അവ ആവശ്യമാണ്." + +#: contrib/auth/forms.py:101 +msgid "E-mail" +msgstr "ഇ-മെയില്‍" + +#: contrib/auth/forms.py:110 +msgid "" +"That e-mail address doesn't have an associated user account. Are you sure " +"you've registered?" +msgstr "ആ ഇ-മെയിലുമായി ബന്ധപ്പെട്ട യൂസര് അക്കൗണ്ടൊന്നും നിലവിലില്ല. രജിസ്റ്റര്‍ ചെയ്തെന്നു തീര്‍ച്ചയാണോ?" + +#: contrib/auth/forms.py:136 +#, python-format +msgid "Password reset on %s" +msgstr "%s ലെ പാസ് വേര്‍ഡ് പുനസ്ഥാപിച്ചു." + +#: contrib/auth/forms.py:145 +msgid "New password confirmation" +msgstr "പുതിയ പാസ് വേര്‍ഡ് ഉറപ്പാക്കല്‍" + +#: contrib/auth/forms.py:178 +msgid "Your old password was entered incorrectly. Please enter it again." +msgstr "നിങ്ങളുടെ പഴയ പാസ് വേര്‍ഡ് തെറ്റായാണ് നല്കിയത്. തിരുത്തുക." + +#: contrib/auth/models.py:66 contrib/auth/models.py:94 +msgid "name" +msgstr "പേര്" + +#: contrib/auth/models.py:68 +msgid "codename" +msgstr "കോഡ്-നാമം" + +#: contrib/auth/models.py:72 +msgid "permission" +msgstr "അനുമതി" + +#: contrib/auth/models.py:73 contrib/auth/models.py:95 +msgid "permissions" +msgstr "അനുമതികള്‍" + +#: contrib/auth/models.py:98 +msgid "group" +msgstr "ഗ്രൂപ്പ്" + +#: contrib/auth/models.py:99 contrib/auth/models.py:206 +msgid "groups" +msgstr "ഗ്രൂപ്പുകള്‍" + +#: contrib/auth/models.py:196 +msgid "username" +msgstr "യൂസര്‍ നാമം (ഉപയോക്ത്രു നാമം)" + +#: contrib/auth/models.py:196 +msgid "" +"Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters" +msgstr "നിര്‍ബന്ധം. 30 ഓ അതില്‍ കുറവോ ചിഹ്നങ്ങള്‍. അക്ഷരങ്ങള്‍, അക്കങ്ങള്‍, " +"പിന്നെ @/./+/-/_എന്നിവയും മാത്രം." + +#: contrib/auth/models.py:197 +msgid "first name" +msgstr "പേര് - ആദ്യഭാഗം" + +#: contrib/auth/models.py:198 +msgid "last name" +msgstr "പേര് - അന്ത്യഭാഗം" + +#: contrib/auth/models.py:199 +msgid "e-mail address" +msgstr "ഇ-മെയില്‍ വിലാസം" + +#: contrib/auth/models.py:200 +msgid "password" +msgstr "പാസ് വേര്‍ഡ്" + +#: contrib/auth/models.py:200 +msgid "" +"Use '[algo]$[salt]$[hexdigest]' or use the change " +"password form." +msgstr "'[algo]$[salt]$[hexdigest]' അല്ലെങ്കില്‍ പാസ് വേര്‍ഡ് " +"മാറ്റാനുള്ള ഫോം ഉപയോഗിക്കുക." + +#: contrib/auth/models.py:201 +msgid "staff status" +msgstr "സ്റ്റാഫ് പദവി" + +#: contrib/auth/models.py:201 +msgid "Designates whether the user can log into this admin site." +msgstr "ഈ യൂസര്‍ക്ക് ഈ അഡ്മിന് സൈറ്റിലേക്ക് പ്രവേശിക്കാമോ എന്നു വ്യക്തമാക്കാന്‍" + +#: contrib/auth/models.py:202 +msgid "active" +msgstr "സജീവം" + +#: contrib/auth/models.py:202 +msgid "" +"Designates whether this user should be treated as active. Unselect this " +"instead of deleting accounts." +msgstr "ഈ യൂസര്‍ സജീവമാണോയെന്ന് വ്യക്തമാക്കുന്നു. അക്കൗണ്ട് ഡിലീറ്റ് ചെയ്യുന്നതിനു പകരം ഇത് ഒഴിവാക്കുക." + +#: contrib/auth/models.py:203 +msgid "superuser status" +msgstr "സൂപ്പര്‍-യൂസര്‍ പദവി" + +#: contrib/auth/models.py:203 +msgid "" +"Designates that this user has all permissions without explicitly assigning " +"them." +msgstr "ഈ ഉപയോക്താവിന് എടുത്തു പറയാതെ തന്നെ എല്ലാ അനുമതികളും ലഭിക്കുന്നതാണെന്ന് വ്യക്തമാക്കുന്നു" + +#: contrib/auth/models.py:204 +msgid "last login" +msgstr "അവസാനമായി ലോഗിന്‍ ചെയ്തതു" + +#: contrib/auth/models.py:205 +msgid "date joined" +msgstr "ചേര്‍ന്ന തീയതി" + +#: contrib/auth/models.py:207 +msgid "" +"In addition to the permissions manually assigned, this user will also get " +"all permissions granted to each group he/she is in." +msgstr "ഈ ഉപയോക്താവിന് നേരിട്ട് ലഭിച്ചതു കൂടാതെ അവര്‍ അംഗമായ ഗ്രൂപ്പിന് ലഭിച്ച അനുമതികളും അനുഭവിക്കാം" + +#: contrib/auth/models.py:208 +msgid "user permissions" +msgstr "യൂസര്‍ (ഉപയോക്താവ്)നുള്ള അനുമതികള്‍" + +#: contrib/auth/models.py:212 contrib/comments/models.py:50 +#: contrib/comments/models.py:168 +msgid "user" +msgstr "യൂസര്‍ (ഉപയോക്താവ്)" + +#: contrib/auth/models.py:213 +msgid "users" +msgstr "യൂസേര്‍സ് (ഉപയോക്താക്കള്‍)" + +#: contrib/auth/models.py:394 +msgid "message" +msgstr "സന്ദേശം" + +#: contrib/auth/views.py:79 +msgid "Logged out" +msgstr "ലോഗ്-ഔട്ട് ചെയ്തു (പുറത്തിറങ്ങി)" + +#: contrib/auth/management/commands/createsuperuser.py:24 +#: core/validators.py:120 forms/fields.py:427 +msgid "Enter a valid e-mail address." +msgstr "ശരിയായ ഇ-മെയില്‍ വിലാസം നല്കുക." + +#: contrib/comments/admin.py:12 +msgid "Content" +msgstr "ഉള്ളടക്കം" + +#: contrib/comments/admin.py:15 +msgid "Metadata" +msgstr "" + +#: contrib/comments/admin.py:40 +msgid "flagged" +msgid_plural "flagged" +msgstr[0] "അടയാളപ്പെടുത്തി" +msgstr[1] "അടയാളപ്പെടുത്തി" + +#: contrib/comments/admin.py:41 +msgid "Flag selected comments" +msgstr "തെരഞ്ഞെടുത്ത അഭിപ്രായങ്ങള്‍ അടയാളപ്പെടുത്തുക" + +#: contrib/comments/admin.py:45 +msgid "approved" +msgid_plural "approved" +msgstr[0] "അംഗീകരിച്ചു" +msgstr[1] "അംഗീകരിച്ചു" + +#: contrib/comments/admin.py:46 +msgid "Approve selected comments" +msgstr "തെരഞ്ഞെടുത്ത അഭിപ്രായങ്ങള്‍ അംഗീകരിക്കുക" + +#: contrib/comments/admin.py:50 +msgid "removed" +msgid_plural "removed" +msgstr[0] "നീക്കം ചെയ്തു" +msgstr[1] "നീക്കം ചെയ്തു" + +#: contrib/comments/admin.py:51 +msgid "Remove selected comments" +msgstr "തെരഞ്ഞെടുത്ത അഭിപ്രായങ്ങള്‍ നീക്കം ചെയ്യുക" + +#: contrib/comments/admin.py:63 +#, python-format +msgid "1 comment was successfully %(action)s." +msgid_plural "%(count)s comments were successfully %(action)s." +msgstr[0] "1 അഭിപ്രായം വിജയകരമായി %(action)s." +msgstr[1] "%(count)s അഭിപ്രായങ്ങള്‍ വിജയകരമായി %(action)s." + +#: contrib/comments/feeds.py:13 +#, python-format +msgid "%(site_name)s comments" +msgstr "%(site_name)s അഭിപ്രായങ്ങള്‍" + +#: contrib/comments/feeds.py:23 +#, python-format +msgid "Latest comments on %(site_name)s" +msgstr "%(site_name)s ലെ ഏറ്റവും പുതിയ അഭിപ്രായങ്ങള്‍" + +#: contrib/comments/forms.py:93 +msgid "Name" +msgstr "പേര്" + +#: contrib/comments/forms.py:94 +msgid "Email address" +msgstr "ഇ-മെയില്‍ വിലാസം" + +#: contrib/comments/forms.py:95 contrib/flatpages/admin.py:8 +#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1101 +msgid "URL" +msgstr "URL(വെബ്-വിലാസം)" + +#: contrib/comments/forms.py:96 +msgid "Comment" +msgstr "അഭിപ്രായം" + +#: contrib/comments/forms.py:175 +#, python-format +msgid "Watch your mouth! The word %s is not allowed here." +msgid_plural "Watch your mouth! The words %s are not allowed here." +msgstr[0] "ശ്ശ്ശ്! %s എന്ന വാക്ക് ഇവിടെ അനുവദനീയമല്ല." +msgstr[1] "ശ്ശ്ശ്! %s എന്നീ വാക്കുകള്‍ ഇവിടെ അനുവദനീയമല്ല." + +#: contrib/comments/forms.py:182 +msgid "" +"If you enter anything in this field your comment will be treated as spam" +msgstr "ഈ കള്ളിയില്‍ എന്തെങ്കിലും രേഖപ്പെടുത്തിയാല്‍ നിങ്ങളുടെ അഭിപ്രായം സ്പാം ആയി കണക്കാക്കും" + +#: contrib/comments/models.py:22 contrib/contenttypes/models.py:81 +msgid "content type" +msgstr "ഏതു തരം ഉള്ളടക്കം" + +#: contrib/comments/models.py:24 +msgid "object ID" +msgstr "വസ്തു ID" + +#: contrib/comments/models.py:52 +msgid "user's name" +msgstr "യൂസറുടെ പേര്" + +#: contrib/comments/models.py:53 +msgid "user's email address" +msgstr "യൂസറുടെ ഇ-മെയില്‍ വിലാസം" + +#: contrib/comments/models.py:54 +msgid "user's URL" +msgstr "യൂസറുടെ URL" + +#: contrib/comments/models.py:56 contrib/comments/models.py:76 +#: contrib/comments/models.py:169 +msgid "comment" +msgstr "അഭിപ്രായം" + +#: contrib/comments/models.py:59 +msgid "date/time submitted" +msgstr "സമര്‍പ്പിച്ച തീയതി/സമയം" + +#: contrib/comments/models.py:60 db/models/fields/__init__.py:896 +msgid "IP address" +msgstr "IP വിലാസം" + +#: contrib/comments/models.py:61 +msgid "is public" +msgstr "പരസ്യമാണ്" + +#: contrib/comments/models.py:62 +msgid "" +"Uncheck this box to make the comment effectively disappear from the site." +msgstr "അഭിപ്രായം സൈറ്റില്‍ നിന്നും ഫലപ്രദമായി നീക്കം ചെയ്യാന്‍ ഈ ബോക്സിലെ ടിക് ഒഴിവാക്കുക." + +#: contrib/comments/models.py:64 +msgid "is removed" +msgstr "നീക്കം ചെയ്തു." + +#: contrib/comments/models.py:65 +msgid "" +"Check this box if the comment is inappropriate. A \"This comment has been " +"removed\" message will be displayed instead." +msgstr "അഭിപ്രായം അനുചിതമെങ്കില്‍ ഈ ബോക്സ് ടിക് ചെയ്യുക. \"ഈ അഭിപ്രായം നീക്കം ചെയ്തു \" എന്ന സന്ദേശം" +"ആയിരിക്കും പകരം കാണുക." + +#: contrib/comments/models.py:77 +msgid "comments" +msgstr "അഭിപ്രായങ്ങള്‍" + +#: contrib/comments/models.py:119 +msgid "" +"This comment was posted by an authenticated user and thus the name is read-" +"only." +msgstr "ഈ അഭിപ്രായം ഒരു അംഗീകൃത യൂസര്‍ രേഖപ്പെടുത്തിയതാണ്. അതിനാല്‍ പേര് വായിക്കാന്‍ മാത്രം." + +#: contrib/comments/models.py:128 +msgid "" +"This comment was posted by an authenticated user and thus the email is read-" +"only." +msgstr "ഈ അഭിപ്രായം ഒരു അംഗീകൃത യൂസര്‍ രേഖപ്പെടുത്തിയതാണ്. അതിനാല്‍ ഇ-മെയില്‍ വിലാസം വായിക്കാന്‍ മാത്രം." + +#: contrib/comments/models.py:153 +#, python-format +msgid "" +"Posted by %(user)s at %(date)s\n" +"\n" +"%(comment)s\n" +"\n" +"http://%(domain)s%(url)s" +msgstr "" +"%(date)sന് %(user)s രേഖപ്പെടുത്തിയത്:\n" +"\n" +"%(comment)s\n" +"\n" +"http://%(domain)s%(url)s" + +#: contrib/comments/models.py:170 +msgid "flag" +msgstr "അടയാളം" + +#: contrib/comments/models.py:171 +msgid "date" +msgstr "തീയതി" + +#: contrib/comments/models.py:181 +msgid "comment flag" +msgstr "അഭിപ്രായ അടയാളം" + +#: contrib/comments/models.py:182 +msgid "comment flags" +msgstr "അഭിപ്രായ അടയാളങ്ങള്‍" + +#: contrib/comments/templates/comments/approve.html:4 +msgid "Approve a comment" +msgstr "അഭിപ്രായം അംഗീകരിക്കൂ" + +#: contrib/comments/templates/comments/approve.html:7 +msgid "Really make this comment public?" +msgstr "ശരിക്കും ഈ അഭിപ്രായം പരസ്യമാക്കണോ?" + +#: contrib/comments/templates/comments/approve.html:12 +msgid "Approve" +msgstr "അംഗീകരിക്കൂ" + +#: contrib/comments/templates/comments/approved.html:4 +msgid "Thanks for approving" +msgstr "അംഗീകരിച്ചതിനു നന്ദി" + +#: contrib/comments/templates/comments/approved.html:7 +#: contrib/comments/templates/comments/deleted.html:7 +#: contrib/comments/templates/comments/flagged.html:7 +msgid "" +"Thanks for taking the time to improve the quality of discussion on our site" +msgstr "നമ്മുടെ സൈറ്റിലെ ചര്‍ച്ചകളുടെ നിലവാരം ഉയര്‍ത്താന്‍ സമയം ചെലവഴിച്ചതിനു നന്ദി." + +#: contrib/comments/templates/comments/delete.html:4 +msgid "Remove a comment" +msgstr "അഭിപ്രായം നീക്കം ചെയ്യൂ" + +#: contrib/comments/templates/comments/delete.html:7 +msgid "Really remove this comment?" +msgstr "ഈ അഭിപ്രായം ശരിക്കും നീക്കം ചെയ്യണോ?" + +#: contrib/comments/templates/comments/deleted.html:4 +msgid "Thanks for removing" +msgstr "നീക്കം ചെയ്തതിനു നന്ദി" + +#: contrib/comments/templates/comments/flag.html:4 +msgid "Flag this comment" +msgstr "ഈ അഭിപ്രായം അടയാളപ്പെടുത്തൂ" + +#: contrib/comments/templates/comments/flag.html:7 +msgid "Really flag this comment?" +msgstr "ഈ അഭിപ്രായം ശരിക്കും അടയാളപ്പെടുത്തണോ?" + +#: contrib/comments/templates/comments/flag.html:12 +msgid "Flag" +msgstr "അടയാളം" + +#: contrib/comments/templates/comments/flagged.html:4 +msgid "Thanks for flagging" +msgstr "അടയാളപ്പെടുത്തിയതിനു നന്ദി" + +#: contrib/comments/templates/comments/form.html:17 +#: contrib/comments/templates/comments/preview.html:32 +msgid "Post" +msgstr "രേഖപ്പെടുത്തൂ" + +#: contrib/comments/templates/comments/form.html:18 +#: contrib/comments/templates/comments/preview.html:33 +msgid "Preview" +msgstr "അവലോകനം" + +#: contrib/comments/templates/comments/posted.html:4 +msgid "Thanks for commenting" +msgstr "അഭിപ്രായം രേഖപ്പെടുത്തിയതിനു നന്ദി" + +#: contrib/comments/templates/comments/posted.html:7 +msgid "Thank you for your comment" +msgstr "അഭിപ്രായത്തിനു നന്ദി" + +#: contrib/comments/templates/comments/preview.html:4 +#: contrib/comments/templates/comments/preview.html:13 +msgid "Preview your comment" +msgstr "അഭിപ്രായം അവലോകനം ചെയ്യുക" + +#: contrib/comments/templates/comments/preview.html:11 +msgid "Please correct the error below" +msgid_plural "Please correct the errors below" +msgstr[0] "ദയവായി താഴെ പറയുന്ന തെറ്റ് തിരുത്തുക" +msgstr[1] "ദയവായി താഴെ പറയുന്ന തെറ്റുകള്‍ തിരുത്തുക" + +#: contrib/comments/templates/comments/preview.html:16 +msgid "Post your comment" +msgstr "അഭിപ്രായം രേഖപ്പെടുത്തുക" + +#: contrib/comments/templates/comments/preview.html:16 +msgid "or make changes" +msgstr "അല്ലെങ്കില്‍ മാറ്റം വരുത്തുക." + +#: contrib/contenttypes/models.py:77 +msgid "python model class name" +msgstr "" + +#: contrib/contenttypes/models.py:82 +msgid "content types" +msgstr "ഉള്ളടക്കം ഏതൊക്കെ തരം" + +#: contrib/flatpages/admin.py:9 +msgid "" +"Example: '/about/contact/'. Make sure to have leading and trailing slashes." +msgstr "ഉദാ: '/about/contact/'. ആദ്യവും അവസാനവും സ്ളാഷുകള്‍ നിര്‍ബന്ധം." + +#: contrib/flatpages/admin.py:11 +msgid "" +"This value must contain only letters, numbers, underscores, dashes or " +"slashes." +msgstr "ഇതില്‍ അക്ഷരങ്ങള്‍, അക്കങ്ങള്‍, അണ്ടര്‍സ്കോര്‍, വരകള്‍, സ്ളാഷുകള്‍ എന്നിവ മാത്രമേ പാടുള്ളൂ. " + +#: contrib/flatpages/admin.py:22 +msgid "Advanced options" +msgstr "ഉയര്‍ന്ന സൗകര്യങ്ങള്‍" + +#: contrib/flatpages/models.py:8 +msgid "title" +msgstr "ശീര്‍ഷകം" + +#: contrib/flatpages/models.py:9 +msgid "content" +msgstr "ഉള്ളടക്കം" + +#: contrib/flatpages/models.py:10 +msgid "enable comments" +msgstr "അഭിപ്രായങ്ങള്‍ അനുവദിക്കുക" + +#: contrib/flatpages/models.py:11 +msgid "template name" +msgstr "ടെമ്പ്ലേറ്റിന്റെ പേര്" + +#: contrib/flatpages/models.py:12 +msgid "" +"Example: 'flatpages/contact_page.html'. If this isn't provided, the system " +"will use 'flatpages/default.html'." +msgstr "" +"ഉദാ: 'flatpages/contact_page.html'. ഇതു നല്കിയില്ലെങ്കില്‍, 'flatpages/default.html' എന്ന " +"വിലാസം ഉപയോഗിക്കപ്പെടും." + +#: contrib/flatpages/models.py:13 +msgid "registration required" +msgstr "രജിസ്ട്രേഷന്‍ ആവശ്യമാണ്" + +#: contrib/flatpages/models.py:13 +msgid "If this is checked, only logged-in users will be able to view the page." +msgstr "ഇതു ടിക് ചെയ്താല്‍ പിന്നെ ലോഗ്-ഇന്‍ ചെയ്ത യൂസര്‍ക്കു മാത്രമേ ഈ പേജ് കാണാന്‍ കഴിയൂ." + +#: contrib/flatpages/models.py:18 +msgid "flat page" +msgstr "ഫ്ളാറ്റ് പേജ്" + +#: contrib/flatpages/models.py:19 +msgid "flat pages" +msgstr "ഫ്ളാറ്റ് പേജുകള്‍" + +#: contrib/formtools/wizard.py:140 +msgid "" +"We apologize, but your form has expired. Please continue filling out the " +"form from this page." +msgstr "ക്ഷമിക്കണം, താങ്കളുടെ ഫോം കാലഹരണപ്പെട്ടു കഴിഞ്ഞു. ദയവായി ഈ പേജിലെ ഫോം പൂരിപ്പിച്ച് തുടരുക." + +#: contrib/gis/db/models/fields.py:50 +msgid "The base GIS field -- maps to the OpenGIS Specification Geometry type." +msgstr "" + +#: contrib/gis/db/models/fields.py:270 +msgid "Point" +msgstr "ബിന്ദു" + +#: contrib/gis/db/models/fields.py:274 +msgid "Line string" +msgstr "" + +#: contrib/gis/db/models/fields.py:278 +msgid "Polygon" +msgstr "ബഹുഭുജം" + +#: contrib/gis/db/models/fields.py:282 +msgid "Multi-point" +msgstr "ബഹുബിന്ദു" + +#: contrib/gis/db/models/fields.py:286 +msgid "Multi-line string" +msgstr "" + +#: contrib/gis/db/models/fields.py:290 +msgid "Multi polygon" +msgstr "ബഹു ബഹുഭുജം" + +#: contrib/gis/db/models/fields.py:294 +msgid "Geometry collection" +msgstr "ജ്യാമിതി ശേഖരം" + +#: contrib/gis/forms/fields.py:17 +msgid "No geometry value provided." +msgstr "" + +#: contrib/gis/forms/fields.py:18 +msgid "Invalid geometry value." +msgstr "" + +#: contrib/gis/forms/fields.py:19 +msgid "Invalid geometry type." +msgstr "" + +#: contrib/gis/forms/fields.py:20 +msgid "" +"An error occurred when transforming the geometry to the SRID of the geometry " +"form field." +msgstr "" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "th" +msgstr "ആം" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "st" +msgstr "ആം" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "nd" +msgstr "ആം" + +#: contrib/humanize/templatetags/humanize.py:19 +msgid "rd" +msgstr "ആം" + +#: contrib/humanize/templatetags/humanize.py:51 +#, python-format +msgid "%(value).1f million" +msgid_plural "%(value).1f million" +msgstr[0] "%(value).1f മില്ല്യണ്‍ (ദശലക്ഷം)" +msgstr[1] "%(value).1f മില്ല്യണ്‍ (ദശലക്ഷം)" + +#: contrib/humanize/templatetags/humanize.py:54 +#, python-format +msgid "%(value).1f billion" +msgid_plural "%(value).1f billion" +msgstr[0] "%(value).1f ബില്ല്യണ്‍ (ശതകോടി)" +msgstr[1] "%(value).1f ബില്ല്യണ്‍ (ശതകോടി)" + +#: contrib/humanize/templatetags/humanize.py:57 +#, python-format +msgid "%(value).1f trillion" +msgid_plural "%(value).1f trillion" +msgstr[0] "%(value).1f ട്രില്ല്യണ്‍ (ലക്ഷം കോടി)" +msgstr[1] "%(value).1f ട്രില്ല്യണ്‍ (ലക്ഷം കോടി)" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "one" +msgstr "ഒന്ന്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "two" +msgstr "രണ്ട്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "three" +msgstr "മൂന്ന്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "four" +msgstr "നാല്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "five" +msgstr "അഞ്ച്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "six" +msgstr "ആറ്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "seven" +msgstr "ഏഴ്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "eight" +msgstr "എട്ട്" + +#: contrib/humanize/templatetags/humanize.py:73 +msgid "nine" +msgstr "ഒന്‍പത്" + +#: contrib/humanize/templatetags/humanize.py:93 +msgid "today" +msgstr "ഇന്ന്" + +#: contrib/humanize/templatetags/humanize.py:95 +msgid "tomorrow" +msgstr "നാളെ" + +#: contrib/humanize/templatetags/humanize.py:97 +msgid "yesterday" +msgstr "ഇന്നലെ" + +#: contrib/localflavor/ar/forms.py:28 +msgid "Enter a postal code in the format NNNN or ANNNNAAA." +msgstr "" + +#: contrib/localflavor/ar/forms.py:50 contrib/localflavor/br/forms.py:92 +#: contrib/localflavor/br/forms.py:131 contrib/localflavor/pe/forms.py:24 +#: contrib/localflavor/pe/forms.py:52 +msgid "This field requires only numbers." +msgstr "" + +#: contrib/localflavor/ar/forms.py:51 +msgid "This field requires 7 or 8 digits." +msgstr "" + +#: contrib/localflavor/ar/forms.py:80 +msgid "Enter a valid CUIT in XX-XXXXXXXX-X or XXXXXXXXXXXX format." +msgstr "" + +#: contrib/localflavor/ar/forms.py:81 +msgid "Invalid CUIT." +msgstr "" + +#: contrib/localflavor/at/at_states.py:5 +msgid "Burgenland" +msgstr "" + +#: contrib/localflavor/at/at_states.py:6 +msgid "Carinthia" +msgstr "" + +#: contrib/localflavor/at/at_states.py:7 +msgid "Lower Austria" +msgstr "" + +#: contrib/localflavor/at/at_states.py:8 +msgid "Upper Austria" +msgstr "" + +#: contrib/localflavor/at/at_states.py:9 +msgid "Salzburg" +msgstr "" + +#: contrib/localflavor/at/at_states.py:10 +msgid "Styria" +msgstr "" + +#: contrib/localflavor/at/at_states.py:11 +msgid "Tyrol" +msgstr "" + +#: contrib/localflavor/at/at_states.py:12 +msgid "Vorarlberg" +msgstr "" + +#: contrib/localflavor/at/at_states.py:13 +msgid "Vienna" +msgstr "" + +#: contrib/localflavor/at/forms.py:20 contrib/localflavor/ch/forms.py:17 +#: contrib/localflavor/no/forms.py:13 +msgid "Enter a zip code in the format XXXX." +msgstr "" + +#: contrib/localflavor/at/forms.py:48 +msgid "Enter a valid Austrian Social Security Number in XXXX XXXXXX format." +msgstr "" + +#: contrib/localflavor/au/forms.py:17 +msgid "Enter a 4 digit post code." +msgstr "" + +#: contrib/localflavor/br/forms.py:17 +msgid "Enter a zip code in the format XXXXX-XXX." +msgstr "" + +#: contrib/localflavor/br/forms.py:26 +msgid "Phone numbers must be in XX-XXXX-XXXX format." +msgstr "" + +#: contrib/localflavor/br/forms.py:54 +msgid "" +"Select a valid brazilian state. That state is not one of the available " +"states." +msgstr "" + +#: contrib/localflavor/br/forms.py:90 +msgid "Invalid CPF number." +msgstr "" + +#: contrib/localflavor/br/forms.py:91 +msgid "This field requires at most 11 digits or 14 characters." +msgstr "" + +#: contrib/localflavor/br/forms.py:130 +msgid "Invalid CNPJ number." +msgstr "" + +#: contrib/localflavor/br/forms.py:132 +msgid "This field requires at least 14 digits" +msgstr "" + +#: contrib/localflavor/ca/forms.py:25 +msgid "Enter a postal code in the format XXX XXX." +msgstr "" + +#: contrib/localflavor/ca/forms.py:96 +msgid "Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format." +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:5 +msgid "Aargau" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:6 +msgid "Appenzell Innerrhoden" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:7 +msgid "Appenzell Ausserrhoden" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:8 +msgid "Basel-Stadt" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:9 +msgid "Basel-Land" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:10 +msgid "Berne" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:11 +msgid "Fribourg" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:12 +msgid "Geneva" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:13 +msgid "Glarus" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:14 +msgid "Graubuenden" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:15 +msgid "Jura" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:16 +msgid "Lucerne" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:17 +msgid "Neuchatel" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:18 +msgid "Nidwalden" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:19 +msgid "Obwalden" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:20 +msgid "Schaffhausen" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:21 +msgid "Schwyz" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:22 +msgid "Solothurn" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:23 +msgid "St. Gallen" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:24 +msgid "Thurgau" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:25 +msgid "Ticino" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:26 +msgid "Uri" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:27 +msgid "Valais" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:28 +msgid "Vaud" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:29 +msgid "Zug" +msgstr "" + +#: contrib/localflavor/ch/ch_states.py:30 +msgid "Zurich" +msgstr "" + +#: contrib/localflavor/ch/forms.py:65 +msgid "" +"Enter a valid Swiss identity or passport card number in X1234567<0 or " +"1234567890 format." +msgstr "" + +#: contrib/localflavor/cl/forms.py:30 +msgid "Enter a valid Chilean RUT." +msgstr "" + +#: contrib/localflavor/cl/forms.py:31 +msgid "Enter a valid Chilean RUT. The format is XX.XXX.XXX-X." +msgstr "" + +#: contrib/localflavor/cl/forms.py:32 +msgid "The Chilean RUT is not valid." +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:8 +msgid "Prague" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:9 +msgid "Central Bohemian Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:10 +msgid "South Bohemian Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:11 +msgid "Pilsen Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:12 +msgid "Carlsbad Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:13 +msgid "Usti Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:14 +msgid "Liberec Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:15 +msgid "Hradec Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:16 +msgid "Pardubice Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:17 +msgid "Vysocina Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:18 +msgid "South Moravian Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:19 +msgid "Olomouc Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:20 +msgid "Zlin Region" +msgstr "" + +#: contrib/localflavor/cz/cz_regions.py:21 +msgid "Moravian-Silesian Region" +msgstr "" + +#: contrib/localflavor/cz/forms.py:28 contrib/localflavor/sk/forms.py:30 +msgid "Enter a postal code in the format XXXXX or XXX XX." +msgstr "" + +#: contrib/localflavor/cz/forms.py:48 +msgid "Enter a birth number in the format XXXXXX/XXXX or XXXXXXXXXX." +msgstr "" + +#: contrib/localflavor/cz/forms.py:49 +msgid "Invalid optional parameter Gender, valid values are 'f' and 'm'" +msgstr "" + +#: contrib/localflavor/cz/forms.py:50 +msgid "Enter a valid birth number." +msgstr "" + +#: contrib/localflavor/cz/forms.py:107 +msgid "Enter a valid IC number." +msgstr "" + +#: contrib/localflavor/de/de_states.py:5 +msgid "Baden-Wuerttemberg" +msgstr "" + +#: contrib/localflavor/de/de_states.py:6 +msgid "Bavaria" +msgstr "" + +#: contrib/localflavor/de/de_states.py:7 +msgid "Berlin" +msgstr "" + +#: contrib/localflavor/de/de_states.py:8 +msgid "Brandenburg" +msgstr "" + +#: contrib/localflavor/de/de_states.py:9 +msgid "Bremen" +msgstr "" + +#: contrib/localflavor/de/de_states.py:10 +msgid "Hamburg" +msgstr "" + +#: contrib/localflavor/de/de_states.py:11 +msgid "Hessen" +msgstr "" + +#: contrib/localflavor/de/de_states.py:12 +msgid "Mecklenburg-Western Pomerania" +msgstr "" + +#: contrib/localflavor/de/de_states.py:13 +msgid "Lower Saxony" +msgstr "" + +#: contrib/localflavor/de/de_states.py:14 +msgid "North Rhine-Westphalia" +msgstr "" + +#: contrib/localflavor/de/de_states.py:15 +msgid "Rhineland-Palatinate" +msgstr "" + +#: contrib/localflavor/de/de_states.py:16 +msgid "Saarland" +msgstr "" + +#: contrib/localflavor/de/de_states.py:17 +msgid "Saxony" +msgstr "" + +#: contrib/localflavor/de/de_states.py:18 +msgid "Saxony-Anhalt" +msgstr "" + +#: contrib/localflavor/de/de_states.py:19 +msgid "Schleswig-Holstein" +msgstr "" + +#: contrib/localflavor/de/de_states.py:20 +msgid "Thuringia" +msgstr "" + +#: contrib/localflavor/de/forms.py:15 contrib/localflavor/fi/forms.py:13 +#: contrib/localflavor/fr/forms.py:16 +msgid "Enter a zip code in the format XXXXX." +msgstr "" + +#: contrib/localflavor/de/forms.py:42 +msgid "" +"Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X " +"format." +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:5 +msgid "Arava" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:6 +msgid "Albacete" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:7 +msgid "Alacant" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:8 +msgid "Almeria" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:9 +msgid "Avila" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:10 +msgid "Badajoz" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:11 +msgid "Illes Balears" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:12 +msgid "Barcelona" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:13 +msgid "Burgos" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:14 +msgid "Caceres" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:15 +msgid "Cadiz" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:16 +msgid "Castello" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:17 +msgid "Ciudad Real" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:18 +msgid "Cordoba" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:19 +msgid "A Coruna" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:20 +msgid "Cuenca" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:21 +msgid "Girona" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:22 +msgid "Granada" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:23 +msgid "Guadalajara" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:24 +msgid "Guipuzkoa" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:25 +msgid "Huelva" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:26 +msgid "Huesca" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:27 +msgid "Jaen" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:28 +msgid "Leon" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:29 +msgid "Lleida" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:30 +#: contrib/localflavor/es/es_regions.py:17 +msgid "La Rioja" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:31 +msgid "Lugo" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:32 +#: contrib/localflavor/es/es_regions.py:18 +msgid "Madrid" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:33 +msgid "Malaga" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:34 +msgid "Murcia" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:35 +msgid "Navarre" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:36 +msgid "Ourense" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:37 +msgid "Asturias" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:38 +msgid "Palencia" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:39 +msgid "Las Palmas" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:40 +msgid "Pontevedra" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:41 +msgid "Salamanca" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:42 +msgid "Santa Cruz de Tenerife" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:43 +#: contrib/localflavor/es/es_regions.py:11 +msgid "Cantabria" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:44 +msgid "Segovia" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:45 +msgid "Seville" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:46 +msgid "Soria" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:47 +msgid "Tarragona" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:48 +msgid "Teruel" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:49 +msgid "Toledo" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:50 +msgid "Valencia" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:51 +msgid "Valladolid" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:52 +msgid "Bizkaia" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:53 +msgid "Zamora" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:54 +msgid "Zaragoza" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:55 +msgid "Ceuta" +msgstr "" + +#: contrib/localflavor/es/es_provinces.py:56 +msgid "Melilla" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:5 +msgid "Andalusia" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:6 +msgid "Aragon" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:7 +msgid "Principality of Asturias" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:8 +msgid "Balearic Islands" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:9 +msgid "Basque Country" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:10 +msgid "Canary Islands" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:12 +msgid "Castile-La Mancha" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:13 +msgid "Castile and Leon" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:14 +msgid "Catalonia" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:15 +msgid "Extremadura" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:16 +msgid "Galicia" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:19 +msgid "Region of Murcia" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:20 +msgid "Foral Community of Navarre" +msgstr "" + +#: contrib/localflavor/es/es_regions.py:21 +msgid "Valencian Community" +msgstr "" + +#: contrib/localflavor/es/forms.py:20 +msgid "Enter a valid postal code in the range and format 01XXX - 52XXX." +msgstr "" + +#: contrib/localflavor/es/forms.py:40 +msgid "" +"Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or " +"9XXXXXXXX." +msgstr "" + +#: contrib/localflavor/es/forms.py:67 +msgid "Please enter a valid NIF, NIE, or CIF." +msgstr "" + +#: contrib/localflavor/es/forms.py:68 +msgid "Please enter a valid NIF or NIE." +msgstr "" + +#: contrib/localflavor/es/forms.py:69 +msgid "Invalid checksum for NIF." +msgstr "" + +#: contrib/localflavor/es/forms.py:70 +msgid "Invalid checksum for NIE." +msgstr "" + +#: contrib/localflavor/es/forms.py:71 +msgid "Invalid checksum for CIF." +msgstr "" + +#: contrib/localflavor/es/forms.py:143 +msgid "" +"Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX." +msgstr "" + +#: contrib/localflavor/es/forms.py:144 +msgid "Invalid checksum for bank account number." +msgstr "" + +#: contrib/localflavor/fi/forms.py:29 +msgid "Enter a valid Finnish social security number." +msgstr "" + +#: contrib/localflavor/fr/forms.py:31 +msgid "Phone numbers must be in 0X XX XX XX XX format." +msgstr "" + +#: contrib/localflavor/id/forms.py:28 +msgid "Enter a valid post code" +msgstr "" + +#: contrib/localflavor/id/forms.py:68 contrib/localflavor/nl/forms.py:53 +msgid "Enter a valid phone number" +msgstr "" + +#: contrib/localflavor/id/forms.py:107 +msgid "Enter a valid vehicle license plate number" +msgstr "" + +#: contrib/localflavor/id/forms.py:170 +msgid "Enter a valid NIK/KTP number" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:9 +#: contrib/localflavor/id/id_choices.py:73 +msgid "Bali" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:10 +#: contrib/localflavor/id/id_choices.py:45 +msgid "Banten" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:11 +#: contrib/localflavor/id/id_choices.py:54 +msgid "Bengkulu" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:12 +#: contrib/localflavor/id/id_choices.py:47 +msgid "Yogyakarta" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:13 +#: contrib/localflavor/id/id_choices.py:51 +msgid "Jakarta" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:14 +#: contrib/localflavor/id/id_choices.py:75 +msgid "Gorontalo" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:15 +#: contrib/localflavor/id/id_choices.py:57 +msgid "Jambi" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:16 +msgid "Jawa Barat" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:17 +msgid "Jawa Tengah" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:18 +msgid "Jawa Timur" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:19 +#: contrib/localflavor/id/id_choices.py:88 +msgid "Kalimantan Barat" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:20 +#: contrib/localflavor/id/id_choices.py:66 +msgid "Kalimantan Selatan" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:21 +#: contrib/localflavor/id/id_choices.py:89 +msgid "Kalimantan Tengah" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:22 +#: contrib/localflavor/id/id_choices.py:90 +msgid "Kalimantan Timur" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:23 +msgid "Kepulauan Bangka-Belitung" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:24 +#: contrib/localflavor/id/id_choices.py:62 +msgid "Kepulauan Riau" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:25 +#: contrib/localflavor/id/id_choices.py:55 +msgid "Lampung" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:26 +#: contrib/localflavor/id/id_choices.py:70 +msgid "Maluku" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:27 +#: contrib/localflavor/id/id_choices.py:71 +msgid "Maluku Utara" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:28 +#: contrib/localflavor/id/id_choices.py:59 +msgid "Nanggroe Aceh Darussalam" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:29 +msgid "Nusa Tenggara Barat" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:30 +msgid "Nusa Tenggara Timur" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:31 +msgid "Papua" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:32 +msgid "Papua Barat" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:33 +#: contrib/localflavor/id/id_choices.py:60 +msgid "Riau" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:34 +#: contrib/localflavor/id/id_choices.py:68 +msgid "Sulawesi Barat" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:35 +#: contrib/localflavor/id/id_choices.py:69 +msgid "Sulawesi Selatan" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:36 +#: contrib/localflavor/id/id_choices.py:76 +msgid "Sulawesi Tengah" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:37 +#: contrib/localflavor/id/id_choices.py:79 +msgid "Sulawesi Tenggara" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:38 +msgid "Sulawesi Utara" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:39 +#: contrib/localflavor/id/id_choices.py:52 +msgid "Sumatera Barat" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:40 +#: contrib/localflavor/id/id_choices.py:56 +msgid "Sumatera Selatan" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:41 +#: contrib/localflavor/id/id_choices.py:58 +msgid "Sumatera Utara" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:46 +msgid "Magelang" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:48 +msgid "Surakarta - Solo" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:49 +msgid "Madiun" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:50 +msgid "Kediri" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:53 +msgid "Tapanuli" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:61 +msgid "Kepulauan Bangka Belitung" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:63 +msgid "Corps Consulate" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:64 +msgid "Corps Diplomatic" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:65 +msgid "Bandung" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:67 +msgid "Sulawesi Utara Daratan" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:72 +msgid "NTT - Timor" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:74 +msgid "Sulawesi Utara Kepulauan" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:77 +msgid "NTB - Lombok" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:78 +msgid "Papua dan Papua Barat" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:80 +msgid "Cirebon" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:81 +msgid "NTB - Sumbawa" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:82 +msgid "NTT - Flores" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:83 +msgid "NTT - Sumba" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:84 +msgid "Bogor" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:85 +msgid "Pekalongan" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:86 +msgid "Semarang" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:87 +msgid "Pati" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:91 +msgid "Surabaya" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:92 +msgid "Madura" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:93 +msgid "Malang" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:94 +msgid "Jember" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:95 +msgid "Banyumas" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:96 +msgid "Federal Government" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:97 +msgid "Bojonegoro" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:98 +msgid "Purwakarta" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:99 +msgid "Sidoarjo" +msgstr "" + +#: contrib/localflavor/id/id_choices.py:100 +msgid "Garut" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:8 +msgid "Antrim" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:9 +msgid "Armagh" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:10 +msgid "Carlow" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:11 +msgid "Cavan" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:12 +msgid "Clare" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:13 +msgid "Cork" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:14 +msgid "Derry" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:15 +msgid "Donegal" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:16 +msgid "Down" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:17 +msgid "Dublin" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:18 +msgid "Fermanagh" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:19 +msgid "Galway" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:20 +msgid "Kerry" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:21 +msgid "Kildare" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:22 +msgid "Kilkenny" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:23 +msgid "Laois" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:24 +msgid "Leitrim" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:25 +msgid "Limerick" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:26 +msgid "Longford" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:27 +msgid "Louth" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:28 +msgid "Mayo" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:29 +msgid "Meath" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:30 +msgid "Monaghan" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:31 +msgid "Offaly" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:32 +msgid "Roscommon" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:33 +msgid "Sligo" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:34 +msgid "Tipperary" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:35 +msgid "Tyrone" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:36 +msgid "Waterford" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:37 +msgid "Westmeath" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:38 +msgid "Wexford" +msgstr "" + +#: contrib/localflavor/ie/ie_counties.py:39 +msgid "Wicklow" +msgstr "" + +#: contrib/localflavor/in_/forms.py:15 +msgid "Enter a zip code in the format XXXXXXX." +msgstr "പിന്‍-കോഡ് XXXXXXX എന്ന മാത്രുകയില്‍ നല്കുക." + +#: contrib/localflavor/is_/forms.py:18 +msgid "" +"Enter a valid Icelandic identification number. The format is XXXXXX-XXXX." +msgstr "" + +#: contrib/localflavor/is_/forms.py:19 +msgid "The Icelandic identification number is not valid." +msgstr "" + +#: contrib/localflavor/it/forms.py:15 +msgid "Enter a valid zip code." +msgstr "" + +#: contrib/localflavor/it/forms.py:44 +msgid "Enter a valid Social Security number." +msgstr "" + +#: contrib/localflavor/it/forms.py:69 +msgid "Enter a valid VAT number." +msgstr "" + +#: contrib/localflavor/jp/forms.py:16 +msgid "Enter a postal code in the format XXXXXXX or XXX-XXXX." +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:4 +msgid "Hokkaido" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:5 +msgid "Aomori" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:6 +msgid "Iwate" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:7 +msgid "Miyagi" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:8 +msgid "Akita" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:9 +msgid "Yamagata" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:10 +msgid "Fukushima" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:11 +msgid "Ibaraki" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:12 +msgid "Tochigi" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:13 +msgid "Gunma" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:14 +msgid "Saitama" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:15 +msgid "Chiba" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:16 +msgid "Tokyo" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:17 +msgid "Kanagawa" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:18 +msgid "Yamanashi" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:19 +msgid "Nagano" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:20 +msgid "Niigata" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:21 +msgid "Toyama" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:22 +msgid "Ishikawa" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:23 +msgid "Fukui" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:24 +msgid "Gifu" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:25 +msgid "Shizuoka" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:26 +msgid "Aichi" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:27 +msgid "Mie" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:28 +msgid "Shiga" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:29 +msgid "Kyoto" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:30 +msgid "Osaka" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:31 +msgid "Hyogo" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:32 +msgid "Nara" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:33 +msgid "Wakayama" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:34 +msgid "Tottori" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:35 +msgid "Shimane" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:36 +msgid "Okayama" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:37 +msgid "Hiroshima" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:38 +msgid "Yamaguchi" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:39 +msgid "Tokushima" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:40 +msgid "Kagawa" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:41 +msgid "Ehime" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:42 +msgid "Kochi" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:43 +msgid "Fukuoka" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:44 +msgid "Saga" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:45 +msgid "Nagasaki" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:46 +msgid "Kumamoto" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:47 +msgid "Oita" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:48 +msgid "Miyazaki" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:49 +msgid "Kagoshima" +msgstr "" + +#: contrib/localflavor/jp/jp_prefectures.py:50 +msgid "Okinawa" +msgstr "" + +#: contrib/localflavor/kw/forms.py:25 +msgid "Enter a valid Kuwaiti Civil ID number" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:12 +msgid "Aguascalientes" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:13 +msgid "Baja California" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:14 +msgid "Baja California Sur" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:15 +msgid "Campeche" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:16 +msgid "Chihuahua" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:17 +msgid "Chiapas" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:18 +msgid "Coahuila" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:19 +msgid "Colima" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:20 +msgid "Distrito Federal" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:21 +msgid "Durango" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:22 +msgid "Guerrero" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:23 +msgid "Guanajuato" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:24 +msgid "Hidalgo" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:25 +msgid "Jalisco" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:26 +msgid "Estado de México" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:27 +msgid "Michoacán" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:28 +msgid "Morelos" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:29 +msgid "Nayarit" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:30 +msgid "Nuevo León" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:31 +msgid "Oaxaca" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:32 +msgid "Puebla" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:33 +msgid "Querétaro" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:34 +msgid "Quintana Roo" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:35 +msgid "Sinaloa" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:36 +msgid "San Luis Potosí" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:37 +msgid "Sonora" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:38 +msgid "Tabasco" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:39 +msgid "Tamaulipas" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:40 +msgid "Tlaxcala" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:41 +msgid "Veracruz" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:42 +msgid "Yucatán" +msgstr "" + +#: contrib/localflavor/mx/mx_states.py:43 +msgid "Zacatecas" +msgstr "" + +#: contrib/localflavor/nl/forms.py:22 +msgid "Enter a valid postal code" +msgstr "" + +#: contrib/localflavor/nl/forms.py:79 +msgid "Enter a valid SoFi number" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:4 +msgid "Drenthe" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:5 +msgid "Flevoland" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:6 +msgid "Friesland" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:7 +msgid "Gelderland" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:8 +msgid "Groningen" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:9 +msgid "Limburg" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:10 +msgid "Noord-Brabant" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:11 +msgid "Noord-Holland" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:12 +msgid "Overijssel" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:13 +msgid "Utrecht" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:14 +msgid "Zeeland" +msgstr "" + +#: contrib/localflavor/nl/nl_provinces.py:15 +msgid "Zuid-Holland" +msgstr "" + +#: contrib/localflavor/no/forms.py:34 +msgid "Enter a valid Norwegian social security number." +msgstr "" + +#: contrib/localflavor/pe/forms.py:25 +msgid "This field requires 8 digits." +msgstr "" + +#: contrib/localflavor/pe/forms.py:53 +msgid "This field requires 11 digits." +msgstr "" + +#: contrib/localflavor/pl/forms.py:38 +msgid "National Identification Number consists of 11 digits." +msgstr "" + +#: contrib/localflavor/pl/forms.py:39 +msgid "Wrong checksum for the National Identification Number." +msgstr "" + +#: contrib/localflavor/pl/forms.py:71 +msgid "" +"Enter a tax number field (NIP) in the format XXX-XXX-XX-XX or XX-XX-XXX-XXX." +msgstr "" + +#: contrib/localflavor/pl/forms.py:72 +msgid "Wrong checksum for the Tax Number (NIP)." +msgstr "" + +#: contrib/localflavor/pl/forms.py:109 +msgid "National Business Register Number (REGON) consists of 9 or 14 digits." +msgstr "" + +#: contrib/localflavor/pl/forms.py:110 +msgid "Wrong checksum for the National Business Register Number (REGON)." +msgstr "" + +#: contrib/localflavor/pl/forms.py:148 +msgid "Enter a postal code in the format XX-XXX." +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:8 +msgid "Lower Silesia" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:9 +msgid "Kuyavia-Pomerania" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:10 +msgid "Lublin" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:11 +msgid "Lubusz" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:12 +msgid "Lodz" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:13 +msgid "Lesser Poland" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:14 +msgid "Masovia" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:15 +msgid "Opole" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:16 +msgid "Subcarpatia" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:17 +msgid "Podlasie" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:18 +msgid "Pomerania" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:19 +msgid "Silesia" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:20 +msgid "Swietokrzyskie" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:21 +msgid "Warmia-Masuria" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:22 +msgid "Greater Poland" +msgstr "" + +#: contrib/localflavor/pl/pl_voivodeships.py:23 +msgid "West Pomerania" +msgstr "" + +#: contrib/localflavor/pt/forms.py:17 +msgid "Enter a zip code in the format XXXX-XXX." +msgstr "" + +#: contrib/localflavor/pt/forms.py:37 +msgid "Phone numbers must have 9 digits, or start by + or 00." +msgstr "" + +#: contrib/localflavor/ro/forms.py:19 +msgid "Enter a valid CIF." +msgstr "" + +#: contrib/localflavor/ro/forms.py:56 +msgid "Enter a valid CNP." +msgstr "" + +#: contrib/localflavor/ro/forms.py:141 +msgid "Enter a valid IBAN in ROXX-XXXX-XXXX-XXXX-XXXX-XXXX format" +msgstr "" + +#: contrib/localflavor/ro/forms.py:171 +msgid "Phone numbers must be in XXXX-XXXXXX format." +msgstr "" + +#: contrib/localflavor/ro/forms.py:194 +msgid "Enter a valid postal code in the format XXXXXX" +msgstr "" + +#: contrib/localflavor/se/forms.py:50 +msgid "Enter a valid Swedish organisation number." +msgstr "" + +#: contrib/localflavor/se/forms.py:107 +msgid "Enter a valid Swedish personal identity number." +msgstr "" + +#: contrib/localflavor/se/forms.py:108 +msgid "Co-ordination numbers are not allowed." +msgstr "" + +#: contrib/localflavor/se/forms.py:150 +msgid "Enter a Swedish postal code in the format XXXXX." +msgstr "" + +#: contrib/localflavor/se/se_counties.py:15 +msgid "Stockholm" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:16 +msgid "Västerbotten" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:17 +msgid "Norrbotten" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:18 +msgid "Uppsala" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:19 +msgid "Södermanland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:20 +msgid "Östergötland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:21 +msgid "Jönköping" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:22 +msgid "Kronoberg" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:23 +msgid "Kalmar" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:24 +msgid "Gotland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:25 +msgid "Blekinge" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:26 +msgid "Skåne" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:27 +msgid "Halland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:28 +msgid "Västra Götaland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:29 +msgid "Värmland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:30 +msgid "Örebro" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:31 +msgid "Västmanland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:32 +msgid "Dalarna" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:33 +msgid "Gävleborg" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:34 +msgid "Västernorrland" +msgstr "" + +#: contrib/localflavor/se/se_counties.py:35 +msgid "Jämtland" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:8 +msgid "Banska Bystrica" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:9 +msgid "Banska Stiavnica" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:10 +msgid "Bardejov" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:11 +msgid "Banovce nad Bebravou" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:12 +msgid "Brezno" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:13 +msgid "Bratislava I" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:14 +msgid "Bratislava II" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:15 +msgid "Bratislava III" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:16 +msgid "Bratislava IV" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:17 +msgid "Bratislava V" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:18 +msgid "Bytca" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:19 +msgid "Cadca" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:20 +msgid "Detva" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:21 +msgid "Dolny Kubin" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:22 +msgid "Dunajska Streda" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:23 +msgid "Galanta" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:24 +msgid "Gelnica" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:25 +msgid "Hlohovec" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:26 +msgid "Humenne" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:27 +msgid "Ilava" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:28 +msgid "Kezmarok" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:29 +msgid "Komarno" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:30 +msgid "Kosice I" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:31 +msgid "Kosice II" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:32 +msgid "Kosice III" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:33 +msgid "Kosice IV" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:34 +msgid "Kosice - okolie" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:35 +msgid "Krupina" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:36 +msgid "Kysucke Nove Mesto" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:37 +msgid "Levice" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:38 +msgid "Levoca" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:39 +msgid "Liptovsky Mikulas" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:40 +msgid "Lucenec" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:41 +msgid "Malacky" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:42 +msgid "Martin" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:43 +msgid "Medzilaborce" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:44 +msgid "Michalovce" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:45 +msgid "Myjava" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:46 +msgid "Namestovo" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:47 +msgid "Nitra" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:48 +msgid "Nove Mesto nad Vahom" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:49 +msgid "Nove Zamky" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:50 +msgid "Partizanske" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:51 +msgid "Pezinok" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:52 +msgid "Piestany" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:53 +msgid "Poltar" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:54 +msgid "Poprad" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:55 +msgid "Povazska Bystrica" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:56 +msgid "Presov" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:57 +msgid "Prievidza" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:58 +msgid "Puchov" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:59 +msgid "Revuca" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:60 +msgid "Rimavska Sobota" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:61 +msgid "Roznava" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:62 +msgid "Ruzomberok" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:63 +msgid "Sabinov" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:64 +msgid "Senec" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:65 +msgid "Senica" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:66 +msgid "Skalica" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:67 +msgid "Snina" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:68 +msgid "Sobrance" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:69 +msgid "Spisska Nova Ves" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:70 +msgid "Stara Lubovna" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:71 +msgid "Stropkov" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:72 +msgid "Svidnik" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:73 +msgid "Sala" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:74 +msgid "Topolcany" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:75 +msgid "Trebisov" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:76 +msgid "Trencin" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:77 +msgid "Trnava" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:78 +msgid "Turcianske Teplice" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:79 +msgid "Tvrdosin" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:80 +msgid "Velky Krtis" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:81 +msgid "Vranov nad Toplou" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:82 +msgid "Zlate Moravce" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:83 +msgid "Zvolen" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:84 +msgid "Zarnovica" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:85 +msgid "Ziar nad Hronom" +msgstr "" + +#: contrib/localflavor/sk/sk_districts.py:86 +msgid "Zilina" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:8 +msgid "Banska Bystrica region" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:9 +msgid "Bratislava region" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:10 +msgid "Kosice region" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:11 +msgid "Nitra region" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:12 +msgid "Presov region" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:13 +msgid "Trencin region" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:14 +msgid "Trnava region" +msgstr "" + +#: contrib/localflavor/sk/sk_regions.py:15 +msgid "Zilina region" +msgstr "" + +#: contrib/localflavor/uk/forms.py:21 +msgid "Enter a valid postcode." +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:11 +msgid "Bedfordshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:12 +msgid "Buckinghamshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:14 +msgid "Cheshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:15 +msgid "Cornwall and Isles of Scilly" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:16 +msgid "Cumbria" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:17 +msgid "Derbyshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:18 +msgid "Devon" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:19 +msgid "Dorset" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:20 +msgid "Durham" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:21 +msgid "East Sussex" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:22 +msgid "Essex" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:23 +msgid "Gloucestershire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:24 +msgid "Greater London" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:25 +msgid "Greater Manchester" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:26 +msgid "Hampshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:27 +msgid "Hertfordshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:28 +msgid "Kent" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:29 +msgid "Lancashire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:30 +msgid "Leicestershire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:31 +msgid "Lincolnshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:32 +msgid "Merseyside" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:33 +msgid "Norfolk" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:34 +msgid "North Yorkshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:35 +msgid "Northamptonshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:36 +msgid "Northumberland" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:37 +msgid "Nottinghamshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:38 +msgid "Oxfordshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:39 +msgid "Shropshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:40 +msgid "Somerset" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:41 +msgid "South Yorkshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:42 +msgid "Staffordshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:43 +msgid "Suffolk" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:44 +msgid "Surrey" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:45 +msgid "Tyne and Wear" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:46 +msgid "Warwickshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:47 +msgid "West Midlands" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:48 +msgid "West Sussex" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:49 +msgid "West Yorkshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:50 +msgid "Wiltshire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:51 +msgid "Worcestershire" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:55 +msgid "County Antrim" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:56 +msgid "County Armagh" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:57 +msgid "County Down" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:58 +msgid "County Fermanagh" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:59 +msgid "County Londonderry" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:60 +msgid "County Tyrone" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:64 +msgid "Clwyd" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:65 +msgid "Dyfed" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:66 +msgid "Gwent" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:67 +msgid "Gwynedd" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:68 +msgid "Mid Glamorgan" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:69 +msgid "Powys" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:70 +msgid "South Glamorgan" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:71 +msgid "West Glamorgan" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:75 +msgid "Borders" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:76 +msgid "Central Scotland" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:77 +msgid "Dumfries and Galloway" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:78 +msgid "Fife" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:79 +msgid "Grampian" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:80 +msgid "Highland" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:81 +msgid "Lothian" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:82 +msgid "Orkney Islands" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:83 +msgid "Shetland Islands" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:84 +msgid "Strathclyde" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:85 +msgid "Tayside" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:86 +msgid "Western Isles" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:90 +msgid "England" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:91 +msgid "Northern Ireland" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:92 +msgid "Scotland" +msgstr "" + +#: contrib/localflavor/uk/uk_regions.py:93 +msgid "Wales" +msgstr "" + +#: contrib/localflavor/us/forms.py:17 +msgid "Enter a zip code in the format XXXXX or XXXXX-XXXX." +msgstr "" + +#: contrib/localflavor/us/forms.py:26 +msgid "Phone numbers must be in XXX-XXX-XXXX format." +msgstr "" + +#: contrib/localflavor/us/forms.py:55 +msgid "Enter a valid U.S. Social Security number in XXX-XX-XXXX format." +msgstr "" + +#: contrib/localflavor/us/forms.py:88 +msgid "Enter a U.S. state or territory." +msgstr "" + +#: contrib/localflavor/us/models.py:8 +msgid "U.S. state (two uppercase letters)" +msgstr "" + +#: contrib/localflavor/us/models.py:17 +msgid "Phone number" +msgstr "" + +#: contrib/localflavor/uy/forms.py:28 +msgid "Enter a valid CI number in X.XXX.XXX-X,XXXXXXX-X or XXXXXXXX format." +msgstr "" + +#: contrib/localflavor/uy/forms.py:30 +msgid "Enter a valid CI number." +msgstr "" + +#: contrib/localflavor/za/forms.py:21 +msgid "Enter a valid South African ID number" +msgstr "" + +#: contrib/localflavor/za/forms.py:55 +msgid "Enter a valid South African postal code" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:4 +msgid "Eastern Cape" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:5 +msgid "Free State" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:6 +msgid "Gauteng" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:7 +msgid "KwaZulu-Natal" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:8 +msgid "Limpopo" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:9 +msgid "Mpumalanga" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:10 +msgid "Northern Cape" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:11 +msgid "North West" +msgstr "" + +#: contrib/localflavor/za/za_provinces.py:12 +msgid "Western Cape" +msgstr "" + +#: contrib/messages/tests/base.py:101 +msgid "lazy message" +msgstr "അലസ സന്ദേശം" + +#: contrib/redirects/models.py:7 +msgid "redirect from" +msgstr "പഴയ വിലാസം" + +#: contrib/redirects/models.py:8 +msgid "" +"This should be an absolute path, excluding the domain name. Example: '/" +"events/search/'." +msgstr "ഇത് ഡൊമൈന്‍ നാമം ഉള്‍പ്പെടാത്ത ഒരു കേവലമാര്‍ഗം (വിലാസം) ആവണം. " +"ഉദാ: '/events/search/'." + +#: contrib/redirects/models.py:9 +msgid "redirect to" +msgstr "പുതിയ വിലാസം" + +#: contrib/redirects/models.py:10 +msgid "" +"This can be either an absolute path (as above) or a full URL starting with " +"'http://'." +msgstr "ഇതൊരു കേവല മാര്‍ഗമോ 'http://' എന്നു തുടങ്ങുന്ന പൂര്‍ണ്ണ വിലാസമോ (URL) ആവാം" + +#: contrib/redirects/models.py:13 +msgid "redirect" +msgstr "വിലാസമാറ്റം" + +#: contrib/redirects/models.py:14 +msgid "redirects" +msgstr "വിലാസമാറ്റങ്ങള്‍" + +#: contrib/sessions/models.py:45 +msgid "session key" +msgstr "സെഷന്‍ കീ" + +#: contrib/sessions/models.py:47 +msgid "session data" +msgstr "സെഷന്‍ വിവരം" + +#: contrib/sessions/models.py:48 +msgid "expire date" +msgstr "കാലാവധി (തീയതി)" + +#: contrib/sessions/models.py:53 +msgid "session" +msgstr "സെഷന്‍" + +#: contrib/sessions/models.py:54 +msgid "sessions" +msgstr "സെഷനുകള്‍" + +#: contrib/sites/models.py:32 +msgid "domain name" +msgstr "ഡൊമൈന്‍ നാമം" + +#: contrib/sites/models.py:33 +msgid "display name" +msgstr "പ്രദര്‍ശന നാമം" + +#: contrib/sites/models.py:39 +msgid "sites" +msgstr "സൈറ്റുകള്‍" + +#: core/validators.py:20 forms/fields.py:66 +msgid "Enter a valid value." +msgstr "ശരിയായ മൂല്യം നല്കണം." + +#: core/validators.py:87 forms/fields.py:528 +msgid "Enter a valid URL." +msgstr "ശരിയായ URL നല്കണം." + +#: core/validators.py:89 forms/fields.py:529 +msgid "This URL appears to be a broken link." +msgstr "ഈ URL നിലവില്ലാത്ത വിലാസമാണ് കാണിക്കുന്നത്." + +#: core/validators.py:123 forms/fields.py:877 +msgid "" +"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "ശരിയായ സ്ളഗ് നല്കുക (അക്ഷരങ്ങള്‍, അക്കങ്ങള്‍, അണ്ടര്‍സ്കോര്‍, ഹൈഫന്‍ എന്നിവ മാത്രം ചേര്‍ന്നത്)." + +#: core/validators.py:126 forms/fields.py:870 +msgid "Enter a valid IPv4 address." +msgstr "ശരിയായ IPv4 വിലാസം നല്കണം" + +#: core/validators.py:129 db/models/fields/__init__.py:572 +msgid "Enter only digits separated by commas." +msgstr "അക്കങ്ങള്‍ മാത്രം (കോമയിട്ടു വേര്‍തിരിച്ചത്)" + +#: core/validators.py:135 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "ഇത് %(limit_value)s ആവണം. (ഇപ്പോള്‍ %(show_value)s)." + +#: core/validators.py:153 forms/fields.py:204 forms/fields.py:256 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "ഇത് %(limit_value)s-ഓ അതില്‍ കുറവോ ആവണം" + +#: core/validators.py:158 forms/fields.py:205 forms/fields.py:257 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "ഇത് %(limit_value)s-ഓ അതില്‍ കൂടുതലോ ആവണം" + +#: core/validators.py:164 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d characters (it has %" +"(show_value)d)." +msgstr "ഇതിനു ഏറ്റവും കുറഞ്ഞത് %(limit_value)d അക്ഷരങ്ങള്‍ വേണം. (ഇപ്പോള്‍ " +"%(show_value)d അക്ഷരങ്ങളുണ്ട്.)" + +#: core/validators.py:170 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d characters (it has %" +"(show_value)d)." +msgstr "ഇതിനു പരമാവധി %(limit_value)d അക്ഷരങ്ങളേ ഉള്ളൂ എന്നു ഉറപ്പാക്കുക. (ഇപ്പോള്‍ " +"%(show_value)d അക്ഷരങ്ങളുണ്ട്.)" + +#: db/models/base.py:823 +#, python-format +msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." +msgstr "%(date_field)s %(lookup)s-നു %(field_name)s ആവര്‍ത്തിക്കാന്‍ പാടില്ല." + +#: db/models/base.py:838 db/models/base.py:846 +#, python-format +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "%(field_label)s-ഓടു കൂടിയ %(model_name)s നിലവിലുണ്ട്." + +#: db/models/fields/__init__.py:63 +#, python-format +msgid "Value %r is not a valid choice." +msgstr "%r അനുയോജ്യമല്ല." + +#: db/models/fields/__init__.py:64 +msgid "This field cannot be null." +msgstr "ഈ കള്ളി ഒഴിച്ചിടരുത്." + +#: db/models/fields/__init__.py:65 +msgid "This field cannot be blank." +msgstr "ഈ കള്ളി ഒഴിച്ചിടരുത്." + +#: db/models/fields/__init__.py:70 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:852 +#: db/models/fields/__init__.py:961 db/models/fields/__init__.py:972 +#: db/models/fields/__init__.py:999 +msgid "Integer" +msgstr "പൂര്‍ണ്ണസംഖ്യ" + +#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:850 +msgid "This value must be an integer." +msgstr "പൂര്‍ണ്ണസംഖ്യ മാത്രം" + +#: db/models/fields/__init__.py:490 +msgid "This value must be either True or False." +msgstr "ശരിയോ തെറ്റോ എന്നു മാത്രം" + +#: db/models/fields/__init__.py:492 +msgid "Boolean (Either True or False)" +msgstr "ശരിയോ തെറ്റോ എന്നു മാത്രം" + +#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:982 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: db/models/fields/__init__.py:567 +msgid "Comma-separated integers" +msgstr "കോമയിട്ട് വേര്‍തിരിച്ച സംഖ്യകള്‍" + +#: db/models/fields/__init__.py:581 +msgid "Date (without time)" +msgstr "തീയതി (സമയം വേണ്ട)" + +#: db/models/fields/__init__.py:585 +msgid "Enter a valid date in YYYY-MM-DD format." +msgstr "ശരിയായ തീയതി YYYY-MM-DD എന്ന മാത്രുകയില്‍ നല്കണം." + +#: db/models/fields/__init__.py:586 +#, python-format +msgid "Invalid date: %s" +msgstr "തെറ്റായ തീയതി: %s" + +#: db/models/fields/__init__.py:667 +msgid "Enter a valid date/time in YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format." +msgstr "ശരിയായ തീയതി/സമയം YYYY-MM-DD HH:MM[:ss[.uuuuuu]]എന്ന മാത്രുകയില്‍ നല്കണം." + +#: db/models/fields/__init__.py:669 +msgid "Date (with time)" +msgstr "തീയതി (സമയത്തോടൊപ്പം)" + +#: db/models/fields/__init__.py:735 +msgid "This value must be a decimal number." +msgstr "ഈ വില ദശാംശമാവണം." + +#: db/models/fields/__init__.py:737 +msgid "Decimal number" +msgstr "ദശാംശസംഖ്യ" + +#: db/models/fields/__init__.py:792 +msgid "E-mail address" +msgstr "ഇ-മെയില്‍ വിലാസം" + +#: db/models/fields/__init__.py:799 db/models/fields/files.py:220 +#: db/models/fields/files.py:331 +msgid "File path" +msgstr "ഫയല്‍ സ്ഥാനം" + +#: db/models/fields/__init__.py:822 +msgid "This value must be a float." +msgstr "ഈ വില ദശാംശമാവണം." + +#: db/models/fields/__init__.py:824 +msgid "Floating point number" +msgstr "ദശാംശസംഖ്യ" + +#: db/models/fields/__init__.py:883 +msgid "Big (8 byte) integer" +msgstr "8 ബൈറ്റ് പൂര്‍ണസംഖ്യ." + +#: db/models/fields/__init__.py:912 +msgid "This value must be either None, True or False." +msgstr "ഈ മൂല്യം None, True, False എന്നിവയില്‍ ഏതെങ്കിലും ഒന്നാവണം" + +#: db/models/fields/__init__.py:914 +msgid "Boolean (Either True, False or None)" +msgstr "ശരിയോ തെറ്റോ എന്നു മാത്രം" + +#: db/models/fields/__init__.py:1005 +msgid "Text" +msgstr "ടെക്സ്റ്റ്" + +#: db/models/fields/__init__.py:1021 +msgid "Time" +msgstr "സമയം" + +#: db/models/fields/__init__.py:1025 +msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." +msgstr "ശരിയായ സമയം HH:MM[:ss[.uuuuuu]] എന്ന മാത്രുകയില്‍ നല്കണം." + +#: db/models/fields/__init__.py:1109 +msgid "XML text" +msgstr "" + +#: db/models/fields/related.py:799 +#, python-format +msgid "Model %(model)s with pk %(pk)r does not exist." +msgstr "" + +#: db/models/fields/related.py:801 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: db/models/fields/related.py:918 +msgid "One-to-one relationship" +msgstr "" + +#: db/models/fields/related.py:980 +msgid "Many-to-many relationship" +msgstr "" + +#: db/models/fields/related.py:1000 +msgid "" +"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "\"Control\" എന്ന കീ അമര്‍ത്തിപ്പിടിക്കുക. (Macലാണെങ്കില്‍ \"Command\")." + +#: db/models/fields/related.py:1061 +#, python-format +msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." +msgid_plural "" +"Please enter valid %(self)s IDs. The values %(value)r are invalid." +msgstr[0] "ദയവായി ശരിയായ %(self)s IDകള്‍ നല്കുക. %(value)r അനുയോജ്യമല്ല." +msgstr[1] "ദയവായി ശരിയായ %(self)s IDകള്‍ നല്കുക. %(value)r അനുയോജ്യമല്ല." + +#: forms/fields.py:65 +msgid "This field is required." +msgstr "ഈ കള്ളി നിര്‍ബന്ധമാണ്." + +#: forms/fields.py:203 +msgid "Enter a whole number." +msgstr "ഒരു പൂര്‍ണസംഖ്യ നല്കുക." + +#: forms/fields.py:234 forms/fields.py:255 +msgid "Enter a number." +msgstr "ഒരു സംഖ്യ നല്കുക." + +#: forms/fields.py:258 +#, python-format +msgid "Ensure that there are no more than %s digits in total." +msgstr "മൊത്തം %s ലേറെ അക്കങ്ങള്‍ ഇല്ലെന്ന് ഉറപ്പു വരുത്തുക." + +#: forms/fields.py:259 +#, python-format +msgid "Ensure that there are no more than %s decimal places." +msgstr "%s ലേറെ ദശാംശസ്ഥാനങ്ങള്‍ ഇല്ലെന്ന് ഉറപ്പു വരുത്തുക." + +#: forms/fields.py:260 +#, python-format +msgid "Ensure that there are no more than %s digits before the decimal point." +msgstr "ദശാംശബിന്ദുവിനു മുമ്പ് %sലേറെ അക്കങ്ങള്‍ ഇല്ലെന്നു ഉറപ്പു വരുത്തുക." + +#: forms/fields.py:322 forms/fields.py:837 +msgid "Enter a valid date." +msgstr "ശരിയായ തീയതി നല്കുക." + +#: forms/fields.py:350 forms/fields.py:838 +msgid "Enter a valid time." +msgstr "ശരിയായ സമയം നല്കുക." + +#: forms/fields.py:376 +msgid "Enter a valid date/time." +msgstr "ശരിയായ തീയതിയും സമയവും നല്കുക." + +#: forms/fields.py:434 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "ഫയലൊന്നും ലഭിച്ചില്ല. ഫോമിലെ എന്‍-കോഡിംഗ് പരിശോധിക്കുക." + +#: forms/fields.py:435 +msgid "No file was submitted." +msgstr "ഫയലൊന്നും ലഭിച്ചില്ല." + +#: forms/fields.py:436 +msgid "The submitted file is empty." +msgstr "ലഭിച്ച ഫയല്‍ ശൂന്യമാണ്." + +#: forms/fields.py:437 +#, python-format +msgid "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "ഈ ഫയലിന്റെ പേര് പരമാവധി %(max)d അക്ഷരങ്ങളുള്ളതായിരിക്കണം. " +"(ഇപ്പോള്‍ %(length)d അക്ഷരങ്ങള്‍ ഉണ്ട്)." + +#: forms/fields.py:472 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "ശരിയായ ചിത്രം അപ് ലോഡ് ചെയ്യുക. നിങ്ങള്‍ നല്കിയ ഫയല്‍ ഒന്നുകില്‍ ഒരു ചിത്രമല്ല, " +"അല്ലെങ്കില്‍ വികലമാണ്." + +#: forms/fields.py:595 forms/fields.py:670 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "യോഗ്യമായത് തെരഞ്ഞെടുക്കുക. %(value)s ലഭ്യമായവയില്‍ ഉള്‍പ്പെടുന്നില്ല." + +#: forms/fields.py:671 forms/fields.py:733 forms/models.py:1002 +msgid "Enter a list of values." +msgstr "മൂല്യങ്ങളുടെ പട്ടിക(ലിസ്റ്റ്) നല്കുക." + +#: forms/formsets.py:298 forms/formsets.py:300 +msgid "Order" +msgstr "ക്രമം" + +#: forms/models.py:562 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "%(field)s-നായി നല്കുന്ന വിവരം ആവര്‍ത്തിച്ചത് ദയവായി തിരുത്തുക." + +#: forms/models.py:566 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "%(field)s-നായി നല്കുന്ന വിവരം ആവര്‍ത്തിക്കാന്‍ പാടില്ല. ദയവായി തിരുത്തുക." + +#: forms/models.py:572 +#, 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 "%(date_field)s ലെ %(lookup)s നു വേണ്ടി %(field_name)s നു നല്കുന്ന വിവരം " +"ആവര്‍ത്തിക്കാന്‍ പാടില്ല. ദയവായി തിരുത്തുക." + +#: forms/models.py:580 +msgid "Please correct the duplicate values below." +msgstr "താഴെ കൊടുത്തവയില്‍ ആവര്‍ത്തനം ഒഴിവാക്കുക." + +#: forms/models.py:855 +msgid "The inline foreign key did not match the parent instance primary key." +msgstr "" + +#: forms/models.py:921 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "യോഗ്യമായത് തെരഞ്ഞെടുക്കുക. നിങ്ങള്‍ നല്കിയത് ലഭ്യമായവയില്‍ ഉള്‍പ്പെടുന്നില്ല." + +#: forms/models.py:1003 +#, python-format +msgid "Select a valid choice. %s is not one of the available choices." +msgstr "യോഗ്യമായത് തെരഞ്ഞെടുക്കുക. %s തന്നിരിക്കുന്നവയില്‍ ഉള്‍പ്പെടുന്നില്ല." + +#: forms/models.py:1005 +#, python-format +msgid "\"%s\" is not a valid value for a primary key." +msgstr "\"%s\" പ്രാഥമിക കീ ആവാന്‍ അനുയോജ്യമായ മൂല്യമല്ല." + +#: template/defaultfilters.py:776 +msgid "yes,no,maybe" +msgstr "ഉണ്ട്, ഇല്ല, ഉണ്ടാവാം" + +#: template/defaultfilters.py:807 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: template/defaultfilters.py:809 +#, python-format +msgid "%s KB" +msgstr "" + +#: template/defaultfilters.py:811 +#, python-format +msgid "%s MB" +msgstr "" + +#: template/defaultfilters.py:812 +#, python-format +msgid "%s GB" +msgstr "" + +#: utils/dateformat.py:42 +msgid "p.m." +msgstr "" + +#: utils/dateformat.py:43 +msgid "a.m." +msgstr "" + +#: utils/dateformat.py:48 +msgid "PM" +msgstr "PM" + +#: utils/dateformat.py:49 +msgid "AM" +msgstr "AM" + +#: utils/dateformat.py:98 +msgid "midnight" +msgstr "അര്‍ധരാത്രി" + +#: utils/dateformat.py:100 +msgid "noon" +msgstr "ഉച്ച" + +#: utils/dates.py:6 +msgid "Monday" +msgstr "തിങ്കള്‍" + +#: utils/dates.py:6 +msgid "Tuesday" +msgstr "ചൊവ്വ" + +#: utils/dates.py:6 +msgid "Wednesday" +msgstr "ബുധന്‍" + +#: utils/dates.py:6 +msgid "Thursday" +msgstr "വ്യാഴം" + +#: utils/dates.py:6 +msgid "Friday" +msgstr "വെള്ളി" + +#: utils/dates.py:7 +msgid "Saturday" +msgstr "ശനി" + +#: utils/dates.py:7 +msgid "Sunday" +msgstr "ഞായര്‍" + +#: utils/dates.py:10 +msgid "Mon" +msgstr "തിങ്കള്‍" + +#: utils/dates.py:10 +msgid "Tue" +msgstr "ചൊവ്വ" + +#: utils/dates.py:10 +msgid "Wed" +msgstr "ബുധന്‍" + +#: utils/dates.py:10 +msgid "Thu" +msgstr "വ്യാഴം" + +#: utils/dates.py:10 +msgid "Fri" +msgstr "വെള്ളി" + +#: utils/dates.py:11 +msgid "Sat" +msgstr "ശനി" + +#: utils/dates.py:11 +msgid "Sun" +msgstr "ഞായര്‍" + +#: utils/dates.py:18 +msgid "January" +msgstr "ജനുവരി" + +#: utils/dates.py:18 +msgid "February" +msgstr "ഫെബ്രുവരി" + +#: utils/dates.py:18 utils/dates.py:31 +msgid "March" +msgstr "മാര്‍ച്ച്" + +#: utils/dates.py:18 utils/dates.py:31 +msgid "April" +msgstr "ഏപ്രില്‍" + +#: utils/dates.py:18 utils/dates.py:31 +msgid "May" +msgstr "മേയ്" + +#: utils/dates.py:18 utils/dates.py:31 +msgid "June" +msgstr "ജൂണ്‍" + +#: utils/dates.py:19 utils/dates.py:31 +msgid "July" +msgstr "ജൂലൈ" + +#: utils/dates.py:19 +msgid "August" +msgstr "ആഗസ്ത്" + +#: utils/dates.py:19 +msgid "September" +msgstr "സെപ്തംബര്‍" + +#: utils/dates.py:19 +msgid "October" +msgstr "ഒക്ടോബര്‍" + +#: utils/dates.py:19 +msgid "November" +msgstr "നവംബര്‍" + +#: utils/dates.py:20 +msgid "December" +msgstr "ഡിസംബര്‍" + +#: utils/dates.py:23 +msgid "jan" +msgstr "ജനു." + +#: utils/dates.py:23 +msgid "feb" +msgstr "ഫെബ്രു." + +#: utils/dates.py:23 +msgid "mar" +msgstr "മാര്‍ച്ച്" + +#: utils/dates.py:23 +msgid "apr" +msgstr "ഏപ്രില്‍" + +#: utils/dates.py:23 +msgid "may" +msgstr "മേയ്" + +#: utils/dates.py:23 +msgid "jun" +msgstr "ജൂണ്‍" + +#: utils/dates.py:24 +msgid "jul" +msgstr "ജൂലൈ" + +#: utils/dates.py:24 +msgid "aug" +msgstr "ആഗസ്ത്" + +#: utils/dates.py:24 +msgid "sep" +msgstr "സെപ്ടം." + +#: utils/dates.py:24 +msgid "oct" +msgstr "ഒക്ടോ." + +#: utils/dates.py:24 +msgid "nov" +msgstr "നവം." + +#: utils/dates.py:24 +msgid "dec" +msgstr "ഡിസം." + +#: utils/dates.py:31 +msgid "Jan." +msgstr "ജനു." + +#: utils/dates.py:31 +msgid "Feb." +msgstr "ഫെബ്രു." + +#: utils/dates.py:32 +msgid "Aug." +msgstr "ആഗസ്ത്" + +#: utils/dates.py:32 +msgid "Sept." +msgstr "സെപ്ടം." + +#: utils/dates.py:32 +msgid "Oct." +msgstr "ഒക്ടോ." + +#: utils/dates.py:32 +msgid "Nov." +msgstr "നവം." + +#: utils/dates.py:32 +msgid "Dec." +msgstr "ഡിസം." + +#: utils/text.py:130 +msgid "or" +msgstr "അഥവാ" + +#: utils/timesince.py:21 +msgid "year" +msgid_plural "years" +msgstr[0] "വര്‍ഷം" +msgstr[1] "വര്‍ഷങ്ങള്‍" + +#: utils/timesince.py:22 +msgid "month" +msgid_plural "months" +msgstr[0] "മാസം" +msgstr[1] "മാസങ്ങള്‍" + +#: utils/timesince.py:23 +msgid "week" +msgid_plural "weeks" +msgstr[0] "ആഴ്ച്ച" +msgstr[1] "ആഴ്ച്ചകള്‍" + +#: utils/timesince.py:24 +msgid "day" +msgid_plural "days" +msgstr[0] "ദിവസം" +msgstr[1] "ദിവസങ്ങള്‍" + +#: utils/timesince.py:25 +msgid "hour" +msgid_plural "hours" +msgstr[0] "മണിക്കൂര്‍" +msgstr[1] "മണിക്കൂറുകള്‍" + +#: utils/timesince.py:26 +msgid "minute" +msgid_plural "minutes" +msgstr[0] "മിനുട്ട്" +msgstr[1] "മിനുട്ടുകള്‍" + +#: utils/timesince.py:45 +msgid "minutes" +msgstr "മിനുട്ടുകള്‍" + +#: utils/timesince.py:50 +#, python-format +msgid "%(number)d %(type)s" +msgstr "" + +#: utils/timesince.py:56 +#, python-format +msgid ", %(number)d %(type)s" +msgstr "" + +#: utils/translation/trans_real.py:519 +msgid "DATE_FORMAT" +msgstr "" + +#: utils/translation/trans_real.py:520 +msgid "DATETIME_FORMAT" +msgstr "" + +#: utils/translation/trans_real.py:521 +msgid "TIME_FORMAT" +msgstr "" + +#: utils/translation/trans_real.py:542 +msgid "YEAR_MONTH_FORMAT" +msgstr "" + +#: utils/translation/trans_real.py:543 +msgid "MONTH_DAY_FORMAT" +msgstr "" + +#: views/generic/create_update.py:115 +#, python-format +msgid "The %(verbose_name)s was created successfully." +msgstr "%(verbose_name)s നെ സ്രുഷ്ടിച്ചു." + +#: views/generic/create_update.py:158 +#, python-format +msgid "The %(verbose_name)s was updated successfully." +msgstr "%(verbose_name)s നെ മെച്ചപ്പെടുത്തി." + +#: views/generic/create_update.py:201 +#, python-format +msgid "The %(verbose_name)s was deleted." +msgstr "%(verbose_name)s ഡിലീറ്റ് ചെയ്യപ്പെട്ടു." diff --git a/django/conf/locale/ml/LC_MESSAGES/djangojs.mo b/django/conf/locale/ml/LC_MESSAGES/djangojs.mo new file mode 100644 index 000000000000..644d8bf94993 Binary files /dev/null and b/django/conf/locale/ml/LC_MESSAGES/djangojs.mo differ diff --git a/django/conf/locale/ml/LC_MESSAGES/djangojs.po b/django/conf/locale/ml/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000000..aedc919ba39d --- /dev/null +++ b/django/conf/locale/ml/LC_MESSAGES/djangojs.po @@ -0,0 +1,156 @@ +# Translation of Django Js files to malayalam. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Rajeesh Nair , 2010. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Django SVN\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-05-28 15:32+0530\n" +"PO-Revision-Date: 2010-05-28 15:45+0530\n" +"Last-Translator: Rajeesh Nair \n" +"Language-Team: Malayalam \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Poedit-Language: Malayalam\n" +"X-Poedit-Country: INDIA\n" + +#: contrib/admin/media/js/SelectFilter2.js:37 +#, perl-format +msgid "Available %s" +msgstr "ലഭ്യമായ %s" + +#: contrib/admin/media/js/SelectFilter2.js:45 +msgid "Choose all" +msgstr "എല്ലാം തെരഞ്ഞെടുക്കുക" + +#: contrib/admin/media/js/SelectFilter2.js:50 +msgid "Add" +msgstr "പുതിയത് ചേര്‍ക്കൂ" + +#: contrib/admin/media/js/SelectFilter2.js:52 +msgid "Remove" +msgstr "നീക്കം ചെയ്യൂ" + +#: contrib/admin/media/js/SelectFilter2.js:57 +#, perl-format +msgid "Chosen %s" +msgstr "തെരഞ്ഞെടുത്ത %s" + +#: contrib/admin/media/js/SelectFilter2.js:58 +msgid "Select your choice(s) and click " +msgstr "ഉചിതമായത് തെരഞ്ഞെടുത്ത ശേഷം ക്ളിക് ചെയ്യൂ" + +#: contrib/admin/media/js/SelectFilter2.js:63 +msgid "Clear all" +msgstr "എല്ലാം ക്ളിയര്‍ ചെയ്യൂ" + +#: contrib/admin/media/js/actions.js:18 +#: contrib/admin/media/js/actions.min.js:1 +msgid "%(sel)s of %(cnt)s selected" +msgid_plural "%(sel)s of %(cnt)s selected" +msgstr[0] "%(cnt)sല്‍ %(sel)s തെരഞ്ഞെടുത്തു" +msgstr[1] "%(cnt)sല്‍ %(sel)s എണ്ണം തെരഞ്ഞെടുത്തു" + +#: contrib/admin/media/js/actions.js:109 +#: contrib/admin/media/js/actions.min.js:5 +msgid "" +"You have unsaved changes on individual editable fields. If you run an " +"action, your unsaved changes will be lost." +msgstr "വരുത്തിയ മാറ്റങ്ങള്‍ സേവ് ചെയ്തിട്ടില്ല. ഒരു ആക്ഷന്‍ പ്രയോഗിച്ചാല്‍ സേവ് ചെയ്യാത്ത " +"മാറ്റങ്ങളെല്ലാം നഷ്ടപ്പെടും." + +#: contrib/admin/media/js/actions.js:121 +#: contrib/admin/media/js/actions.min.js:6 +msgid "" +"You have selected an action, but you haven't saved your changes to " +"individual fields yet. Please click OK to save. You'll need to re-run the " +"action." +msgstr "" +"നിങ്ങള്‍ ഒരു ആക്ഷന്‍ തെരഞ്ഞെടുത്തിട്ടുണ്ട്. പക്ഷേ, കളങ്ങളിലെ മാറ്റങ്ങള്‍ ഇനിയും സേവ് ചെയ്യാനുണ്ട്. ആദ്യം സേവ്" +"ചെയ്യാനായി OK ക്ലിക് ചെയ്യുക. അതിനു ശേഷം ആക്ഷന്‍ ഒന്നു കൂടി പ്രയോഗിക്കേണ്ടി വരും." + +#: contrib/admin/media/js/actions.js:123 +#: contrib/admin/media/js/actions.min.js:6 +msgid "" +"You have selected an action, and you haven't made any changes on individual " +"fields. You're probably looking for the Go button rather than the Save " +"button." +msgstr "" +"നിങ്ങള്‍ ഒരു ആക്ഷന്‍ തെരഞ്ഞെടുത്തിട്ടുണ്ട്. കളങ്ങളില്‍ സേവ് ചെയ്യാത്ത മാറ്റങ്ങള്‍ ഇല്ല. നിങ്ങള്‍" +"സേവ് ബട്ടണ്‍ തന്നെയാണോ അതോ ഗോ ബട്ടണാണോ ഉദ്ദേശിച്ചത്." + +#: contrib/admin/media/js/calendar.js:24 +#: contrib/admin/media/js/dateparse.js:32 +msgid "" +"January February March April May June July August September October November " +"December" +msgstr "ജനുവരി ഫെബൃവരി മാര്‍ച്ച് ഏപ്രില്‍ മെയ് ജൂണ്‍ ജൂലൈ ആഗസ്ത് സെപ്തംബര്‍ ഒക്ടോബര്‍ നവംബര്‍ ഡിസംബര്‍" + +#: contrib/admin/media/js/calendar.js:25 +msgid "S M T W T F S" +msgstr "ഞാ തി ചൊ ബു വ്യാ വെ ശ" + +#: contrib/admin/media/js/collapse.js:9 contrib/admin/media/js/collapse.js:21 +#: contrib/admin/media/js/collapse.min.js:1 +msgid "Show" +msgstr "കാണട്ടെ" + +#: contrib/admin/media/js/collapse.js:16 +#: contrib/admin/media/js/collapse.min.js:1 +msgid "Hide" +msgstr "മറയട്ടെ" + +#: contrib/admin/media/js/dateparse.js:33 +msgid "Sunday Monday Tuesday Wednesday Thursday Friday Saturday" +msgstr "ഞായര്‍ തിങ്കള്‍ ചൊവ്വ ബുധന്‍ വ്യാഴം വെള്ളി ശനി" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:48 +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:83 +msgid "Now" +msgstr "ഇപ്പോള്‍" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:52 +msgid "Clock" +msgstr "ഘടികാരം (ക്ലോക്ക്)" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:79 +msgid "Choose a time" +msgstr "സമയം തെരഞ്ഞെടുക്കൂ" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:84 +msgid "Midnight" +msgstr "അര്‍ധരാത്രി" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:85 +msgid "6 a.m." +msgstr "6 a.m." + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:86 +msgid "Noon" +msgstr "ഉച്ച" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:90 +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:187 +msgid "Cancel" +msgstr "റദ്ദാക്കൂ" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:132 +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:181 +msgid "Today" +msgstr "ഇന്ന്" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:136 +msgid "Calendar" +msgstr "കലണ്ടര്‍" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:179 +msgid "Yesterday" +msgstr "ഇന്നലെ" + +#: contrib/admin/media/js/admin/DateTimeShortcuts.js:183 +msgid "Tomorrow" +msgstr "നാളെ" diff --git a/tests/modeltests/m2o_recursive2/__init__.py b/django/conf/locale/ml/__init__.py similarity index 100% rename from tests/modeltests/m2o_recursive2/__init__.py rename to django/conf/locale/ml/__init__.py diff --git a/django/conf/locale/ml/formats.py b/django/conf/locale/ml/formats.py new file mode 100644 index 000000000000..141f705fb9b1 --- /dev/null +++ b/django/conf/locale/ml/formats.py @@ -0,0 +1,38 @@ +# -*- encoding: utf-8 -*- +# This file is distributed under the same license as the Django package. +# + +DATE_FORMAT = 'N j, Y' +TIME_FORMAT = 'P' +DATETIME_FORMAT = 'N j, Y, P' +YEAR_MONTH_FORMAT = 'F Y' +MONTH_DAY_FORMAT = 'F j' +SHORT_DATE_FORMAT = 'm/d/Y' +SHORT_DATETIME_FORMAT = 'm/d/Y P' +FIRST_DAY_OF_WEEK = 0 # Sunday +DATE_INPUT_FORMATS = ( + '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' + # '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' + # '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006' + # '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006' + # '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006' +) +TIME_INPUT_FORMATS = ( + '%H:%M:%S', # '14:30:59' + '%H:%M', # '14:30' +) +DATETIME_INPUT_FORMATS = ( + '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' + '%Y-%m-%d %H:%M', # '2006-10-25 14:30' + '%Y-%m-%d', # '2006-10-25' + '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59' + '%m/%d/%Y %H:%M', # '10/25/2006 14:30' + '%m/%d/%Y', # '10/25/2006' + '%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59' + '%m/%d/%y %H:%M', # '10/25/06 14:30' + '%m/%d/%y', # '10/25/06' +) +DECIMAL_SEPARATOR = '.' +THOUSAND_SEPARATOR = ',' +NUMBER_GROUPING = 3 + diff --git a/django/conf/locale/mn/LC_MESSAGES/django.mo b/django/conf/locale/mn/LC_MESSAGES/django.mo index a90442c9dde6..fd85d0eabebd 100644 Binary files a/django/conf/locale/mn/LC_MESSAGES/django.mo and b/django/conf/locale/mn/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/mn/LC_MESSAGES/django.po b/django/conf/locale/mn/LC_MESSAGES/django.po index 751ad73801fd..38715c631b35 100644 --- a/django/conf/locale/mn/LC_MESSAGES/django.po +++ b/django/conf/locale/mn/LC_MESSAGES/django.po @@ -1357,8 +1357,8 @@ msgid "" "Use '[algo]$[salt]$[hexdigest]' or use the change " "password form." msgstr "" -"Нууц үгийнхээ хэлбэр -ийг өөрчлөхдөө '[algo]$[salt]$[hexdigest]' буюу хэрэглэнэ үү." +"Нууц үгийнхээ хэлбэр-ийг өөрчлөхдөө '[algo]$[salt]$[hexdigest]' буюу хэрэглэнэ үү." #: contrib/auth/models.py:201 msgid "staff status" @@ -4921,18 +4921,18 @@ msgstr[1] "%(size)d байт" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/nb/LC_MESSAGES/django.mo b/django/conf/locale/nb/LC_MESSAGES/django.mo index cbd961ebc3d5..8ad8316b8062 100644 Binary files a/django/conf/locale/nb/LC_MESSAGES/django.mo and b/django/conf/locale/nb/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/nb/LC_MESSAGES/django.po b/django/conf/locale/nb/LC_MESSAGES/django.po index fc4b547fb476..9f8f6d2e1966 100644 --- a/django/conf/locale/nb/LC_MESSAGES/django.po +++ b/django/conf/locale/nb/LC_MESSAGES/django.po @@ -4825,18 +4825,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/nl/LC_MESSAGES/django.mo b/django/conf/locale/nl/LC_MESSAGES/django.mo index ae72be945cbb..c7a4835487a2 100644 Binary files a/django/conf/locale/nl/LC_MESSAGES/django.mo and b/django/conf/locale/nl/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/nl/LC_MESSAGES/django.po b/django/conf/locale/nl/LC_MESSAGES/django.po index 8f56114430a9..96b5319f8260 100644 --- a/django/conf/locale/nl/LC_MESSAGES/django.po +++ b/django/conf/locale/nl/LC_MESSAGES/django.po @@ -4139,18 +4139,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:800 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:802 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:803 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/nn/LC_MESSAGES/django.mo b/django/conf/locale/nn/LC_MESSAGES/django.mo index a3898d06cbee..ad7cb5456b42 100644 Binary files a/django/conf/locale/nn/LC_MESSAGES/django.mo and b/django/conf/locale/nn/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/nn/LC_MESSAGES/django.po b/django/conf/locale/nn/LC_MESSAGES/django.po index 07ee018fad40..fae54c58f746 100644 --- a/django/conf/locale/nn/LC_MESSAGES/django.po +++ b/django/conf/locale/nn/LC_MESSAGES/django.po @@ -4801,18 +4801,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/no/LC_MESSAGES/django.mo b/django/conf/locale/no/LC_MESSAGES/django.mo index 8522ac0c9ab9..263114eaa8d7 100644 Binary files a/django/conf/locale/no/LC_MESSAGES/django.mo and b/django/conf/locale/no/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/no/LC_MESSAGES/django.po b/django/conf/locale/no/LC_MESSAGES/django.po index 02bc100e68fb..5641b9de808d 100644 --- a/django/conf/locale/no/LC_MESSAGES/django.po +++ b/django/conf/locale/no/LC_MESSAGES/django.po @@ -4826,18 +4826,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/pl/LC_MESSAGES/django.mo b/django/conf/locale/pl/LC_MESSAGES/django.mo index 200e9f065ee7..f4bb8f31d64a 100644 Binary files a/django/conf/locale/pl/LC_MESSAGES/django.mo and b/django/conf/locale/pl/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/pl/LC_MESSAGES/django.po b/django/conf/locale/pl/LC_MESSAGES/django.po index 9ced49a5dd23..37f52037b1db 100644 --- a/django/conf/locale/pl/LC_MESSAGES/django.po +++ b/django/conf/locale/pl/LC_MESSAGES/django.po @@ -4815,18 +4815,18 @@ msgstr[2] "%(size)d bajtów" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/pt/LC_MESSAGES/django.mo b/django/conf/locale/pt/LC_MESSAGES/django.mo index 07c776d04a9a..dbfee47807a2 100644 Binary files a/django/conf/locale/pt/LC_MESSAGES/django.mo and b/django/conf/locale/pt/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/pt/LC_MESSAGES/django.po b/django/conf/locale/pt/LC_MESSAGES/django.po index bffff8b59b57..006f04a65d68 100644 --- a/django/conf/locale/pt/LC_MESSAGES/django.po +++ b/django/conf/locale/pt/LC_MESSAGES/django.po @@ -4842,17 +4842,17 @@ msgstr[1] "" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" +msgid "%s KB" msgstr "" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" +msgid "%s MB" msgstr "" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" +msgid "%s GB" msgstr "" #: utils/dateformat.py:42 diff --git a/django/conf/locale/pt_BR/LC_MESSAGES/django.mo b/django/conf/locale/pt_BR/LC_MESSAGES/django.mo index 9ee29fdb4182..a56fbb6299e9 100644 Binary files a/django/conf/locale/pt_BR/LC_MESSAGES/django.mo and b/django/conf/locale/pt_BR/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/pt_BR/LC_MESSAGES/django.po b/django/conf/locale/pt_BR/LC_MESSAGES/django.po index 6eb801354484..68133c641c7f 100644 --- a/django/conf/locale/pt_BR/LC_MESSAGES/django.po +++ b/django/conf/locale/pt_BR/LC_MESSAGES/django.po @@ -4811,18 +4811,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/ro/LC_MESSAGES/django.mo b/django/conf/locale/ro/LC_MESSAGES/django.mo index 697a18ef2776..3f8014bc8bc5 100644 Binary files a/django/conf/locale/ro/LC_MESSAGES/django.mo and b/django/conf/locale/ro/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ro/LC_MESSAGES/django.po b/django/conf/locale/ro/LC_MESSAGES/django.po index 8f2902f7d2a7..dc52b377de81 100644 --- a/django/conf/locale/ro/LC_MESSAGES/django.po +++ b/django/conf/locale/ro/LC_MESSAGES/django.po @@ -3936,18 +3936,18 @@ msgstr[1] "%(size)d bytes" #: .\template\defaultfilters.py:731 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: .\template\defaultfilters.py:733 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: .\template\defaultfilters.py:734 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: .\utils\dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/ru/LC_MESSAGES/django.mo b/django/conf/locale/ru/LC_MESSAGES/django.mo index 3341babb4eb2..340a92127348 100644 Binary files a/django/conf/locale/ru/LC_MESSAGES/django.mo and b/django/conf/locale/ru/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/ru/LC_MESSAGES/django.po b/django/conf/locale/ru/LC_MESSAGES/django.po index f69ae44fc8f8..f2ce79658638 100644 --- a/django/conf/locale/ru/LC_MESSAGES/django.po +++ b/django/conf/locale/ru/LC_MESSAGES/django.po @@ -4707,18 +4707,18 @@ msgstr[2] "%(size)d байт" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f КБ" +msgid "%s KB" +msgstr "%s КБ" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f МБ" +msgid "%s MB" +msgstr "%s МБ" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f ГБ" +msgid "%s GB" +msgstr "%s ГБ" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/sk/LC_MESSAGES/django.mo b/django/conf/locale/sk/LC_MESSAGES/django.mo index 86149fdccfbd..d2f6b993165a 100644 Binary files a/django/conf/locale/sk/LC_MESSAGES/django.mo and b/django/conf/locale/sk/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/sk/LC_MESSAGES/django.po b/django/conf/locale/sk/LC_MESSAGES/django.po index f6e14578dcdb..d122e4e2e788 100644 --- a/django/conf/locale/sk/LC_MESSAGES/django.po +++ b/django/conf/locale/sk/LC_MESSAGES/django.po @@ -4831,18 +4831,18 @@ msgstr[2] "%(size)d bajtov" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/sl/LC_MESSAGES/django.mo b/django/conf/locale/sl/LC_MESSAGES/django.mo index 61ea031d6c97..dcce29cc560c 100644 Binary files a/django/conf/locale/sl/LC_MESSAGES/django.mo and b/django/conf/locale/sl/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/sl/LC_MESSAGES/django.po b/django/conf/locale/sl/LC_MESSAGES/django.po index 4b8bff40f2c2..766ad57826b7 100644 --- a/django/conf/locale/sl/LC_MESSAGES/django.po +++ b/django/conf/locale/sl/LC_MESSAGES/django.po @@ -4863,18 +4863,18 @@ msgstr[3] "%(size)d bajtov" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/sq/LC_MESSAGES/django.po b/django/conf/locale/sq/LC_MESSAGES/django.po index 4750c511488f..1d02b567522d 100644 --- a/django/conf/locale/sq/LC_MESSAGES/django.po +++ b/django/conf/locale/sq/LC_MESSAGES/django.po @@ -106,17 +106,17 @@ msgstr[1] "" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f GB" +msgid "%s GB" msgstr "" #: template/defaultfilters.py:808 #, python-format -msgid "%.1f KB" +msgid "%s KB" msgstr "" #: template/defaultfilters.py:810 #, python-format -msgid "%.1f MB" +msgid "%s MB" msgstr "" #: contrib/admin/sites.py:447 diff --git a/django/conf/locale/sr/LC_MESSAGES/django.mo b/django/conf/locale/sr/LC_MESSAGES/django.mo index 3b4121580c9a..7f8d14d609bd 100644 Binary files a/django/conf/locale/sr/LC_MESSAGES/django.mo and b/django/conf/locale/sr/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/sr/LC_MESSAGES/django.po b/django/conf/locale/sr/LC_MESSAGES/django.po index 51e9ab54dd88..f57b48d04fc7 100644 --- a/django/conf/locale/sr/LC_MESSAGES/django.po +++ b/django/conf/locale/sr/LC_MESSAGES/django.po @@ -4,17 +4,18 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-07 20:44+0200\n" -"PO-Revision-Date: 2010-03-23 23:39+0100\n" +"POT-Creation-Date: 2010-08-06 19:53+0200\n" +"PO-Revision-Date: 2010-08-06 19:47+0100\n" "Last-Translator: Janos Guljas \n" "Language-Team: Branko Vukelic & Janos Guljas " " & Nesh & Petar \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%" -"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: conf/global_settings.py:44 msgid "Arabic" @@ -69,7 +70,7 @@ msgid "Spanish" msgstr "шпански" #: conf/global_settings.py:57 -msgid "Argentinean Spanish" +msgid "Argentinian Spanish" msgstr "аргентински шпански" #: conf/global_settings.py:58 @@ -121,138 +122,146 @@ msgid "Hungarian" msgstr "мађарски" #: conf/global_settings.py:70 +msgid "Indonesian" +msgstr "индонежански" + +#: conf/global_settings.py:71 msgid "Icelandic" msgstr "исландски" -#: conf/global_settings.py:71 +#: conf/global_settings.py:72 msgid "Italian" msgstr "италијански" -#: conf/global_settings.py:72 +#: conf/global_settings.py:73 msgid "Japanese" msgstr "јапански" -#: conf/global_settings.py:73 +#: conf/global_settings.py:74 msgid "Georgian" msgstr "грузијски" -#: conf/global_settings.py:74 +#: conf/global_settings.py:75 msgid "Khmer" msgstr "камбодијски" -#: conf/global_settings.py:75 +#: conf/global_settings.py:76 msgid "Kannada" msgstr "канада" -#: conf/global_settings.py:76 +#: conf/global_settings.py:77 msgid "Korean" msgstr "корејски" -#: conf/global_settings.py:77 +#: conf/global_settings.py:78 msgid "Lithuanian" msgstr "литвански" -#: conf/global_settings.py:78 +#: conf/global_settings.py:79 msgid "Latvian" msgstr "латвијски" -#: conf/global_settings.py:79 +#: conf/global_settings.py:80 msgid "Macedonian" msgstr "македонски" -#: conf/global_settings.py:80 +#: conf/global_settings.py:81 +msgid "Malayalam" +msgstr "малајаламски" + +#: conf/global_settings.py:82 msgid "Mongolian" msgstr "монголски" -#: conf/global_settings.py:81 +#: conf/global_settings.py:83 msgid "Dutch" msgstr "холандски" -#: conf/global_settings.py:82 +#: conf/global_settings.py:84 msgid "Norwegian" msgstr "норвешки" -#: conf/global_settings.py:83 +#: conf/global_settings.py:85 msgid "Norwegian Bokmal" msgstr "норвешки кнјжевни" -#: conf/global_settings.py:84 +#: conf/global_settings.py:86 msgid "Norwegian Nynorsk" msgstr "норвешки нови" -#: conf/global_settings.py:85 +#: conf/global_settings.py:87 msgid "Polish" msgstr "пољски" -#: conf/global_settings.py:86 +#: conf/global_settings.py:88 msgid "Portuguese" msgstr "португалски" -#: conf/global_settings.py:87 +#: conf/global_settings.py:89 msgid "Brazilian Portuguese" msgstr "бразилски португалски" -#: conf/global_settings.py:88 +#: conf/global_settings.py:90 msgid "Romanian" msgstr "румунски" -#: conf/global_settings.py:89 +#: conf/global_settings.py:91 msgid "Russian" msgstr "руски" -#: conf/global_settings.py:90 +#: conf/global_settings.py:92 msgid "Slovak" msgstr "словачки" -#: conf/global_settings.py:91 +#: conf/global_settings.py:93 msgid "Slovenian" msgstr "словеначки" -#: conf/global_settings.py:92 +#: conf/global_settings.py:94 msgid "Albanian" msgstr "албански" -#: conf/global_settings.py:93 +#: conf/global_settings.py:95 msgid "Serbian" msgstr "српски" -#: conf/global_settings.py:94 +#: conf/global_settings.py:96 msgid "Serbian Latin" msgstr "српски (латиница)" -#: conf/global_settings.py:95 +#: conf/global_settings.py:97 msgid "Swedish" msgstr "шведски" -#: conf/global_settings.py:96 +#: conf/global_settings.py:98 msgid "Tamil" msgstr "тамилски" -#: conf/global_settings.py:97 +#: conf/global_settings.py:99 msgid "Telugu" msgstr "телугу" -#: conf/global_settings.py:98 +#: conf/global_settings.py:100 msgid "Thai" msgstr "тајландски" -#: conf/global_settings.py:99 +#: conf/global_settings.py:101 msgid "Turkish" msgstr "турски" -#: conf/global_settings.py:100 +#: conf/global_settings.py:102 msgid "Ukrainian" msgstr "украјински" -#: conf/global_settings.py:101 +#: conf/global_settings.py:103 msgid "Vietnamese" msgstr "вијетнамски" -#: conf/global_settings.py:102 +#: conf/global_settings.py:104 msgid "Simplified Chinese" msgstr "новокинески" -#: conf/global_settings.py:103 +#: conf/global_settings.py:105 msgid "Traditional Chinese" msgstr "старокинески" @@ -304,15 +313,15 @@ msgstr "Овај месец" msgid "This year" msgstr "Ова година" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "Yes" msgstr "Да" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 forms/widgets.py:478 msgid "No" msgstr "Не" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:154 forms/widgets.py:478 msgid "Unknown" msgstr "Непознато" @@ -358,7 +367,7 @@ msgid "Changed %s." msgstr "Измењена поља %s" #: contrib/admin/options.py:559 contrib/admin/options.py:569 -#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:844 +#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:845 #: forms/models.py:568 msgid "and" msgstr "и" @@ -457,9 +466,9 @@ msgstr[1] "%(total_count)s изабрано" msgstr[2] "%(total_count)s изабраних" #: contrib/admin/options.py:1071 -#, fuzzy, python-format +#, python-format msgid "0 of %(cnt)s selected" -msgstr "0 од %(cnt)d изабрано" +msgstr "0 од %(cnt)s изабрано" #: contrib/admin/options.py:1118 #, python-format @@ -687,7 +696,7 @@ msgid "Filter" msgstr "Филтер" #: contrib/admin/templates/admin/delete_confirmation.html:10 -#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:302 +#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:300 msgid "Delete" msgstr "Обриши" @@ -849,7 +858,7 @@ msgstr "Сачувај и додај следећи" msgid "Save and continue editing" msgstr "Сачувај и настави са изменама" -#: contrib/admin/templates/admin/auth/user/add_form.html:5 +#: contrib/admin/templates/admin/auth/user/add_form.html:6 msgid "" "First, enter a username and password. Then, you'll be able to edit more user " "options." @@ -857,6 +866,10 @@ msgstr "" "Прво унесите корисничко име и лозинку. Потом ћете моћи да мењате још " "корисничких подешавања." +#: contrib/admin/templates/admin/auth/user/add_form.html:8 +msgid "Enter a username and password." +msgstr "Унесите корисничко име и лозинку" + #: contrib/admin/templates/admin/auth/user/change_password.html:28 #, python-format msgid "Enter a new password for the user %(username)s." @@ -1050,7 +1063,7 @@ msgstr "Имејл адреса:" msgid "Reset my password" msgstr "Ресетуј моју лозинку" -#: contrib/admin/templatetags/admin_list.py:239 +#: contrib/admin/templatetags/admin_list.py:257 msgid "All dates" msgstr "Сви датуми" @@ -1424,8 +1437,8 @@ msgstr "порука" msgid "Logged out" msgstr "Одјављен" -#: contrib/auth/management/commands/createsuperuser.py:23 -#: core/validators.py:120 forms/fields.py:428 +#: contrib/auth/management/commands/createsuperuser.py:24 +#: core/validators.py:120 forms/fields.py:427 msgid "Enter a valid e-mail address." msgstr "Унесите важећу имејл адресу." @@ -1497,7 +1510,7 @@ msgid "Email address" msgstr "Имејл адреса" #: contrib/comments/forms.py:95 contrib/flatpages/admin.py:8 -#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1101 +#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1109 msgid "URL" msgstr "URL" @@ -1547,7 +1560,7 @@ msgstr "коментар" msgid "date/time submitted" msgstr "датум/време постављања" -#: contrib/comments/models.py:60 db/models/fields/__init__.py:896 +#: contrib/comments/models.py:60 db/models/fields/__init__.py:904 msgid "IP address" msgstr "IP адреса" @@ -4471,22 +4484,22 @@ msgstr "сајтови" msgid "Enter a valid value." msgstr "Унесите исправну вредност." -#: core/validators.py:87 forms/fields.py:529 +#: core/validators.py:87 forms/fields.py:528 msgid "Enter a valid URL." msgstr "Унесите исправан URL." -#: core/validators.py:89 forms/fields.py:530 +#: core/validators.py:89 forms/fields.py:529 msgid "This URL appears to be a broken link." msgstr "Овај URL изгледа не води никуда." -#: core/validators.py:123 forms/fields.py:873 +#: core/validators.py:123 forms/fields.py:877 msgid "" "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." msgstr "" "Унесите исрпаван „слаг“, који се састоји од слова, бројки, доњих црта или " "циртица." -#: core/validators.py:126 forms/fields.py:866 +#: core/validators.py:126 forms/fields.py:870 msgid "Enter a valid IPv4 address." msgstr "Унесите исправну IPv4 адресу." @@ -4499,12 +4512,12 @@ msgstr "Унесите само бројке раздвојене запетам msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "Ово поље мора да буде %(limit_value)s (тренутно има %(show_value)s)." -#: core/validators.py:153 forms/fields.py:205 forms/fields.py:257 +#: core/validators.py:153 forms/fields.py:204 forms/fields.py:256 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Ова вредност мора да буде мања од %(limit_value)s. или тачно толико." -#: core/validators.py:158 forms/fields.py:206 forms/fields.py:258 +#: core/validators.py:158 forms/fields.py:205 forms/fields.py:257 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Ова вредност мора бити већа од %(limit_value)s или тачно толико." @@ -4512,27 +4525,27 @@ msgstr "Ова вредност мора бити већа од %(limit_value)s #: core/validators.py:164 #, python-format msgid "" -"Ensure this value has at least %(limit_value)d characters (it has %" -"(show_value)d)." +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." msgstr "" -"Ово поље мора да садржи најмање %(limit_value)d словних места (тренутно има %" -"(show_value)d)." +"Ово поље мора да садржи најмање %(limit_value)d словних места (тренутно има " +"%(show_value)d)." #: core/validators.py:170 #, python-format msgid "" -"Ensure this value has at most %(limit_value)d characters (it has %" -"(show_value)d)." +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." msgstr "" -"Ово поље мора да садржи највише %(limit_value)d словних места (тренутно има %" -"(show_value)d)." +"Ово поље мора да садржи највише %(limit_value)d словних места (тренутно има " +"%(show_value)d)." -#: db/models/base.py:822 +#: db/models/base.py:823 #, python-format msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "%(field_name)s мора да буде јединствен за %(date_field)s %(lookup)s." -#: db/models/base.py:837 db/models/base.py:845 +#: db/models/base.py:838 db/models/base.py:846 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "%(model_name)s са овом вредношћу %(field_label)s већ постоји." @@ -4555,13 +4568,13 @@ msgstr "Ово поље не може да остане празно." msgid "Field of type: %(field_type)s" msgstr "Поње типа: %(field_type)s" -#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:852 -#: db/models/fields/__init__.py:961 db/models/fields/__init__.py:972 -#: db/models/fields/__init__.py:999 +#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:860 +#: db/models/fields/__init__.py:969 db/models/fields/__init__.py:980 +#: db/models/fields/__init__.py:1007 msgid "Integer" msgstr "Цео број" -#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:850 +#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:858 msgid "This value must be an integer." msgstr "Ова вредност мора бити целобројна." @@ -4573,7 +4586,7 @@ msgstr "Ова вредност мора бити True или False." msgid "Boolean (Either True or False)" msgstr "Булова вредност (True или False)" -#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:982 +#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:990 #, python-format msgid "String (up to %(max_length)s)" msgstr "Стринг (највише %(max_length)s знакова)" @@ -4615,44 +4628,44 @@ msgstr "Децимални број" msgid "E-mail address" msgstr "Имејл адреса" -#: db/models/fields/__init__.py:799 db/models/fields/files.py:220 +#: db/models/fields/__init__.py:807 db/models/fields/files.py:220 #: db/models/fields/files.py:331 msgid "File path" msgstr "Путања фајла" -#: db/models/fields/__init__.py:822 +#: db/models/fields/__init__.py:830 msgid "This value must be a float." msgstr "Ова вредност мора бити број са клизећом запетом" -#: db/models/fields/__init__.py:824 +#: db/models/fields/__init__.py:832 msgid "Floating point number" msgstr "Број са покреном запетом" -#: db/models/fields/__init__.py:883 +#: db/models/fields/__init__.py:891 msgid "Big (8 byte) integer" msgstr "Велики цео број" -#: db/models/fields/__init__.py:912 +#: db/models/fields/__init__.py:920 msgid "This value must be either None, True or False." msgstr "Ова вредност мора бити или None, или True, или False." -#: db/models/fields/__init__.py:914 +#: db/models/fields/__init__.py:922 msgid "Boolean (Either True, False or None)" msgstr "Булова вредност (True, False или None)" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1013 msgid "Text" msgstr "Текст" -#: db/models/fields/__init__.py:1021 +#: db/models/fields/__init__.py:1029 msgid "Time" msgstr "Време" -#: db/models/fields/__init__.py:1025 +#: db/models/fields/__init__.py:1033 msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." msgstr "Унесите исправно време у формату ЧЧ:ММ[:сс[.уууууу]]." -#: db/models/fields/__init__.py:1109 +#: db/models/fields/__init__.py:1125 msgid "XML text" msgstr "XML текст" @@ -4665,22 +4678,22 @@ msgstr "Објекат класе %(model)s са примарним кључем msgid "Foreign Key (type determined by related field)" msgstr "Страни кључ (тип одређује референтно поље)" -#: db/models/fields/related.py:918 +#: db/models/fields/related.py:919 msgid "One-to-one relationship" msgstr "Релација један на један" -#: db/models/fields/related.py:980 +#: db/models/fields/related.py:981 msgid "Many-to-many relationship" msgstr "Релација више на више" -#: db/models/fields/related.py:1000 +#: db/models/fields/related.py:1001 msgid "" "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." msgstr "" "Држите „Control“, или „Command“ на Mac-у да бисте обележили више од једне " "ставке." -#: db/models/fields/related.py:1061 +#: db/models/fields/related.py:1062 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." msgid_plural "" @@ -4693,62 +4706,62 @@ msgstr[2] "Унесите исправан %(self)s IDs. Вредности %(va msgid "This field is required." msgstr "Ово поље се мора попунити." -#: forms/fields.py:204 +#: forms/fields.py:203 msgid "Enter a whole number." msgstr "Унесите цео број." -#: forms/fields.py:235 forms/fields.py:256 +#: forms/fields.py:234 forms/fields.py:255 msgid "Enter a number." msgstr "Унесите број." -#: forms/fields.py:259 +#: forms/fields.py:258 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Не сме бити укупно више од %s цифара. Проверите." -#: forms/fields.py:260 +#: forms/fields.py:259 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Не сме бити укупно више од %s децималних места. Проверите." -#: forms/fields.py:261 +#: forms/fields.py:260 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Не сме бити укупно више од %s цифара пре запете. Проверите." -#: forms/fields.py:323 forms/fields.py:838 +#: forms/fields.py:322 forms/fields.py:837 msgid "Enter a valid date." msgstr "Унесите исправан датум." -#: forms/fields.py:351 forms/fields.py:839 +#: forms/fields.py:350 forms/fields.py:838 msgid "Enter a valid time." msgstr "Унесите исправно време" -#: forms/fields.py:377 +#: forms/fields.py:376 msgid "Enter a valid date/time." msgstr "Унесите исправан датум/време." -#: forms/fields.py:435 +#: forms/fields.py:434 msgid "No file was submitted. Check the encoding type on the form." msgstr "Фајл није пребачен. Проверите тип енкодирања формулара." -#: forms/fields.py:436 +#: forms/fields.py:435 msgid "No file was submitted." msgstr "Фајл није пребачен." -#: forms/fields.py:437 +#: forms/fields.py:436 msgid "The submitted file is empty." msgstr "Пребачен фајл је празан." -#: forms/fields.py:438 +#: forms/fields.py:437 #, python-format msgid "" "Ensure this filename has at most %(max)d characters (it has %(length)d)." msgstr "" -"Назив фајла мора да садржи бар %(max)d словних места (тренутно има %(length)" -"d)." +"Назив фајла мора да садржи бар %(max)d словних места (тренутно има " +"%(length)d)." -#: forms/fields.py:473 +#: forms/fields.py:472 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." @@ -4756,17 +4769,17 @@ msgstr "" "Пребаците исправан фајл. Фајл који је пребачен или није слика, или је " "оштећен." -#: forms/fields.py:596 forms/fields.py:671 +#: forms/fields.py:595 forms/fields.py:670 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "%(value)s није међу понуђеним вредностима. Одаберите једну од понуђених." -#: forms/fields.py:672 forms/fields.py:734 forms/models.py:1002 +#: forms/fields.py:671 forms/fields.py:733 forms/models.py:1002 msgid "Enter a list of values." msgstr "Унесите листу вредности." -#: forms/formsets.py:298 forms/formsets.py:300 +#: forms/formsets.py:296 forms/formsets.py:298 msgid "Order" msgstr "Редослед" @@ -4826,18 +4839,18 @@ msgstr[2] "%(size)d бајтова" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." @@ -5103,23 +5116,23 @@ msgstr "%(number)d %(type)s" msgid ", %(number)d %(type)s" msgstr ", %(number)d %(type)s" -#: utils/translation/trans_real.py:518 +#: utils/translation/trans_real.py:519 msgid "DATE_FORMAT" msgstr "j. F Y." -#: utils/translation/trans_real.py:519 +#: utils/translation/trans_real.py:520 msgid "DATETIME_FORMAT" msgstr "j. F Y. H:i Т" -#: utils/translation/trans_real.py:520 +#: utils/translation/trans_real.py:521 msgid "TIME_FORMAT" msgstr "G:i" -#: utils/translation/trans_real.py:541 +#: utils/translation/trans_real.py:542 msgid "YEAR_MONTH_FORMAT" msgstr "F Y." -#: utils/translation/trans_real.py:542 +#: utils/translation/trans_real.py:543 msgid "MONTH_DAY_FORMAT" msgstr "j. F" diff --git a/django/conf/locale/sr/LC_MESSAGES/djangojs.mo b/django/conf/locale/sr/LC_MESSAGES/djangojs.mo index 6de25a218c32..b74e78cc4b8b 100644 Binary files a/django/conf/locale/sr/LC_MESSAGES/djangojs.mo and b/django/conf/locale/sr/LC_MESSAGES/djangojs.mo differ diff --git a/django/conf/locale/sr/LC_MESSAGES/djangojs.po b/django/conf/locale/sr/LC_MESSAGES/djangojs.po index 56a550b3759d..aecb4267dddd 100644 --- a/django/conf/locale/sr/LC_MESSAGES/djangojs.po +++ b/django/conf/locale/sr/LC_MESSAGES/djangojs.po @@ -4,50 +4,24 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-07 20:46+0200\n" +"POT-Creation-Date: 2010-08-06 19:53+0200\n" "PO-Revision-Date: 2009-03-30 14:04+0200\n" "Last-Translator: Janos Guljas \n" "Language-Team: Branko Vukelic & Janos Guljas " " & Nesh & Petar \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%" -"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" - -#: contrib/admin/media/js/SelectFilter2.js:37 -#, perl-format -msgid "Available %s" -msgstr "Доступни %s" - -#: contrib/admin/media/js/SelectFilter2.js:45 -msgid "Choose all" -msgstr "Додај све" - -#: contrib/admin/media/js/SelectFilter2.js:50 -msgid "Add" -msgstr "Додај" - -#: contrib/admin/media/js/SelectFilter2.js:52 -msgid "Remove" -msgstr "Уклони" - -#: contrib/admin/media/js/SelectFilter2.js:57 -#, perl-format -msgid "Chosen %s" -msgstr "Одабрани %s" - -#: contrib/admin/media/js/SelectFilter2.js:58 -msgid "Select your choice(s) and click " -msgstr "Направите избор и кликните " +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: contrib/admin/media/js/SelectFilter2.js:63 msgid "Clear all" msgstr "Врати све" #: contrib/admin/media/js/actions.js:18 -#: contrib/admin/media/js/actions.min.js:1 msgid "%(sel)s of %(cnt)s selected" msgid_plural "%(sel)s of %(cnt)s selected" msgstr[0] "%(sel)s од %(cnt)s изабран" @@ -55,7 +29,6 @@ msgstr[1] "%(sel)s од %(cnt)s изабрано" msgstr[2] "%(sel)s од %(cnt)s изабраних" #: contrib/admin/media/js/actions.js:109 -#: contrib/admin/media/js/actions.min.js:5 msgid "" "You have unsaved changes on individual editable fields. If you run an " "action, your unsaved changes will be lost." @@ -151,3 +124,21 @@ msgstr "Јуче" #: contrib/admin/media/js/admin/DateTimeShortcuts.js:184 msgid "Tomorrow" msgstr "Сутра" + +#~ msgid "Available %s" +#~ msgstr "Доступни %s" + +#~ msgid "Choose all" +#~ msgstr "Додај све" + +#~ msgid "Add" +#~ msgstr "Додај" + +#~ msgid "Remove" +#~ msgstr "Уклони" + +#~ msgid "Chosen %s" +#~ msgstr "Одабрани %s" + +#~ msgid "Select your choice(s) and click " +#~ msgstr "Направите избор и кликните " diff --git a/django/conf/locale/sr/formats.py b/django/conf/locale/sr/formats.py index 63a20f4574a2..cb0478ed0f59 100644 --- a/django/conf/locale/sr/formats.py +++ b/django/conf/locale/sr/formats.py @@ -11,9 +11,9 @@ SHORT_DATETIME_FORMAT = 'j.m.Y. H:i' FIRST_DAY_OF_WEEK = 1 DATE_INPUT_FORMATS = ( - '%Y-%m-%d', # '2006-10-25' '%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.' '%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.' + '%Y-%m-%d', # '2006-10-25' # '%d. %b %y.', '%d. %B %y.', # '25. Oct 06.', '25. October 06.' # '%d. %b \'%y.', '%d. %B \'%y.', # '25. Oct '06.', '25. October '06.' # '%d. %b %Y.', '%d. %B %Y.', # '25. Oct 2006.', '25. October 2006.' @@ -23,9 +23,6 @@ '%H:%M', # '14:30' ) DATETIME_INPUT_FORMATS = ( - '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' - '%Y-%m-%d %H:%M', # '2006-10-25 14:30' - '%Y-%m-%d', # '2006-10-25' '%d.%m.%Y. %H:%M:%S', # '25.10.2006. 14:30:59' '%d.%m.%Y. %H:%M', # '25.10.2006. 14:30' '%d.%m.%Y.', # '25.10.2006.' @@ -38,7 +35,10 @@ '%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59' '%d. %m. %y. %H:%M', # '25. 10. 06. 14:30' '%d. %m. %y.', # '25. 10. 06.' + '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' + '%Y-%m-%d %H:%M', # '2006-10-25 14:30' + '%Y-%m-%d', # '2006-10-25' ) -DECIMAL_SEPARATOR = '.' -THOUSAND_SEPARATOR = ',' +DECIMAL_SEPARATOR = ',' +THOUSAND_SEPARATOR = '.' NUMBER_GROUPING = 3 diff --git a/django/conf/locale/sr_Latn/LC_MESSAGES/django.mo b/django/conf/locale/sr_Latn/LC_MESSAGES/django.mo index ad6193b38b3e..fdd276016253 100644 Binary files a/django/conf/locale/sr_Latn/LC_MESSAGES/django.mo and b/django/conf/locale/sr_Latn/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/sr_Latn/LC_MESSAGES/django.po b/django/conf/locale/sr_Latn/LC_MESSAGES/django.po index 7947647f8ce2..4f3aeb3422f1 100644 --- a/django/conf/locale/sr_Latn/LC_MESSAGES/django.po +++ b/django/conf/locale/sr_Latn/LC_MESSAGES/django.po @@ -4,17 +4,15 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-05-07 20:46+0200\n" -"PO-Revision-Date: 2010-03-23 23:39+0100\n" +"POT-Creation-Date: 2010-08-06 19:43+0200\n" +"PO-Revision-Date: 2010-08-06 19:47+0100\n" "Last-Translator: Janos Guljas \n" -"Language-Team: Branko Vukelic & Janos Guljas " -" & Nesh & Petar \n" +"Language-Team: Branko Vukelic & Janos Guljas & Nesh & Petar \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%" -"10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #: conf/global_settings.py:44 msgid "Arabic" @@ -69,7 +67,7 @@ msgid "Spanish" msgstr "španski" #: conf/global_settings.py:57 -msgid "Argentinean Spanish" +msgid "Argentinian Spanish" msgstr "argentinski španski" #: conf/global_settings.py:58 @@ -121,138 +119,146 @@ msgid "Hungarian" msgstr "mađarski" #: conf/global_settings.py:70 +msgid "Indonesian" +msgstr "indonežanski" + +#: conf/global_settings.py:71 msgid "Icelandic" msgstr "islandski" -#: conf/global_settings.py:71 +#: conf/global_settings.py:72 msgid "Italian" msgstr "italijanski" -#: conf/global_settings.py:72 +#: conf/global_settings.py:73 msgid "Japanese" msgstr "japanski" -#: conf/global_settings.py:73 +#: conf/global_settings.py:74 msgid "Georgian" msgstr "gruzijski" -#: conf/global_settings.py:74 +#: conf/global_settings.py:75 msgid "Khmer" msgstr "kambodijski" -#: conf/global_settings.py:75 +#: conf/global_settings.py:76 msgid "Kannada" msgstr "kanada" -#: conf/global_settings.py:76 +#: conf/global_settings.py:77 msgid "Korean" msgstr "korejski" -#: conf/global_settings.py:77 +#: conf/global_settings.py:78 msgid "Lithuanian" msgstr "litvanski" -#: conf/global_settings.py:78 +#: conf/global_settings.py:79 msgid "Latvian" msgstr "latvijski" -#: conf/global_settings.py:79 +#: conf/global_settings.py:80 msgid "Macedonian" msgstr "makedonski" -#: conf/global_settings.py:80 +#: conf/global_settings.py:81 +msgid "Malayalam" +msgstr "malajalamski" + +#: conf/global_settings.py:82 msgid "Mongolian" msgstr "mongolski" -#: conf/global_settings.py:81 +#: conf/global_settings.py:83 msgid "Dutch" msgstr "holandski" -#: conf/global_settings.py:82 +#: conf/global_settings.py:84 msgid "Norwegian" msgstr "norveški" -#: conf/global_settings.py:83 +#: conf/global_settings.py:85 msgid "Norwegian Bokmal" msgstr "norveški knjževni" -#: conf/global_settings.py:84 +#: conf/global_settings.py:86 msgid "Norwegian Nynorsk" msgstr "norveški novi" -#: conf/global_settings.py:85 +#: conf/global_settings.py:87 msgid "Polish" msgstr "poljski" -#: conf/global_settings.py:86 +#: conf/global_settings.py:88 msgid "Portuguese" msgstr "portugalski" -#: conf/global_settings.py:87 +#: conf/global_settings.py:89 msgid "Brazilian Portuguese" msgstr "brazilski portugalski" -#: conf/global_settings.py:88 +#: conf/global_settings.py:90 msgid "Romanian" msgstr "rumunski" -#: conf/global_settings.py:89 +#: conf/global_settings.py:91 msgid "Russian" msgstr "ruski" -#: conf/global_settings.py:90 +#: conf/global_settings.py:92 msgid "Slovak" msgstr "slovački" -#: conf/global_settings.py:91 +#: conf/global_settings.py:93 msgid "Slovenian" msgstr "slovenački" -#: conf/global_settings.py:92 +#: conf/global_settings.py:94 msgid "Albanian" msgstr "albanski" -#: conf/global_settings.py:93 +#: conf/global_settings.py:95 msgid "Serbian" msgstr "srpski" -#: conf/global_settings.py:94 +#: conf/global_settings.py:96 msgid "Serbian Latin" msgstr "srpski (latinica)" -#: conf/global_settings.py:95 +#: conf/global_settings.py:97 msgid "Swedish" msgstr "švedski" -#: conf/global_settings.py:96 +#: conf/global_settings.py:98 msgid "Tamil" msgstr "tamilski" -#: conf/global_settings.py:97 +#: conf/global_settings.py:99 msgid "Telugu" msgstr "telugu" -#: conf/global_settings.py:98 +#: conf/global_settings.py:100 msgid "Thai" msgstr "tajlandski" -#: conf/global_settings.py:99 +#: conf/global_settings.py:101 msgid "Turkish" msgstr "turski" -#: conf/global_settings.py:100 +#: conf/global_settings.py:102 msgid "Ukrainian" msgstr "ukrajinski" -#: conf/global_settings.py:101 +#: conf/global_settings.py:103 msgid "Vietnamese" msgstr "vijetnamski" -#: conf/global_settings.py:102 +#: conf/global_settings.py:104 msgid "Simplified Chinese" msgstr "novokineski" -#: conf/global_settings.py:103 +#: conf/global_settings.py:105 msgid "Traditional Chinese" msgstr "starokineski" @@ -261,7 +267,8 @@ msgstr "starokineski" msgid "Successfully deleted %(count)d %(items)s." msgstr "Uspešno obrisano: %(count)d %(items)s." -#: contrib/admin/actions.py:55 contrib/admin/options.py:1125 +#: contrib/admin/actions.py:55 +#: contrib/admin/options.py:1125 msgid "Are you sure?" msgstr "Da li ste sigurni?" @@ -279,8 +286,10 @@ msgstr "" "

      %s:

      \n" "
        \n" -#: contrib/admin/filterspecs.py:75 contrib/admin/filterspecs.py:92 -#: contrib/admin/filterspecs.py:147 contrib/admin/filterspecs.py:173 +#: contrib/admin/filterspecs.py:75 +#: contrib/admin/filterspecs.py:92 +#: contrib/admin/filterspecs.py:147 +#: contrib/admin/filterspecs.py:173 msgid "All" msgstr "Svi" @@ -304,15 +313,18 @@ msgstr "Ovaj mesec" msgid "This year" msgstr "Ova godina" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 +#: forms/widgets.py:478 msgid "Yes" msgstr "Da" -#: contrib/admin/filterspecs.py:147 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:147 +#: forms/widgets.py:478 msgid "No" msgstr "Ne" -#: contrib/admin/filterspecs.py:154 forms/widgets.py:469 +#: contrib/admin/filterspecs.py:154 +#: forms/widgets.py:478 msgid "Unknown" msgstr "Nepoznato" @@ -348,7 +360,8 @@ msgstr "zapis u logovima" msgid "log entries" msgstr "zapisi u logovima" -#: contrib/admin/options.py:138 contrib/admin/options.py:153 +#: contrib/admin/options.py:138 +#: contrib/admin/options.py:153 msgid "None" msgstr "Ništa" @@ -357,8 +370,10 @@ msgstr "Ništa" msgid "Changed %s." msgstr "Izmenjena polja %s" -#: contrib/admin/options.py:559 contrib/admin/options.py:569 -#: contrib/comments/templates/comments/preview.html:16 db/models/base.py:844 +#: contrib/admin/options.py:559 +#: contrib/admin/options.py:569 +#: contrib/comments/templates/comments/preview.html:16 +#: db/models/base.py:845 #: forms/models.py:568 msgid "and" msgstr "i" @@ -387,11 +402,13 @@ msgstr "Bez izmena u poljima." msgid "The %(name)s \"%(obj)s\" was added successfully." msgstr "Objekat „%(obj)s“ klase %(name)s sačuvan je uspešno." -#: contrib/admin/options.py:647 contrib/admin/options.py:680 +#: contrib/admin/options.py:647 +#: contrib/admin/options.py:680 msgid "You may edit it again below." msgstr "Dole možete ponovo da unosite izmene." -#: contrib/admin/options.py:657 contrib/admin/options.py:690 +#: contrib/admin/options.py:657 +#: contrib/admin/options.py:690 #, python-format msgid "You may add another %s below." msgstr "Dole možete da dodate novi objekat klase %s" @@ -403,19 +420,13 @@ msgstr "Objekat „%(obj)s“ klase %(name)s izmenjen je uspešno." #: contrib/admin/options.py:686 #, python-format -msgid "" -"The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." -msgstr "" -"Objekat „%(obj)s“ klase %(name)s dodat je uspešno. Dole možete uneti dodatne " -"izmene." +msgid "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." +msgstr "Objekat „%(obj)s“ klase %(name)s dodat je uspešno. Dole možete uneti dodatne izmene." -#: contrib/admin/options.py:740 contrib/admin/options.py:997 -msgid "" -"Items must be selected in order to perform actions on them. No items have " -"been changed." -msgstr "" -"Potrebno je izabrati objekte da bi se izvršila akcija nad njima. Nijedan " -"objekat nije promenjen." +#: contrib/admin/options.py:740 +#: contrib/admin/options.py:997 +msgid "Items must be selected in order to perform actions on them. No items have been changed." +msgstr "Potrebno je izabrati objekte da bi se izvršila akcija nad njima. Nijedan objekat nije promenjen." #: contrib/admin/options.py:759 msgid "No action selected." @@ -426,7 +437,8 @@ msgstr "Nije izabrana nijedna akcija." msgid "Add %s" msgstr "Dodaj objekat klase %s" -#: contrib/admin/options.py:866 contrib/admin/options.py:1105 +#: contrib/admin/options.py:866 +#: contrib/admin/options.py:1105 #, python-format msgid "%(name)s object with primary key %(key)r does not exist." msgstr "Objekat klase %(name)s sa primarnim ključem %(key)r ne postoji." @@ -457,9 +469,9 @@ msgstr[1] "%(total_count)s izabrano" msgstr[2] "%(total_count)s izabranih" #: contrib/admin/options.py:1071 -#, fuzzy, python-format +#, python-format msgid "0 of %(cnt)s selected" -msgstr "0 od %(cnt)d izabrano" +msgstr "0 od %(cnt)s izabrano" #: contrib/admin/options.py:1118 #, python-format @@ -471,33 +483,30 @@ msgstr "Objekat „%(obj)s“ klase %(name)s uspešno je obrisan." msgid "Change history: %s" msgstr "Istorijat izmena: %s" -#: contrib/admin/sites.py:18 contrib/admin/views/decorators.py:14 +#: contrib/admin/sites.py:18 +#: contrib/admin/views/decorators.py:14 #: contrib/auth/forms.py:81 -msgid "" -"Please enter a correct username and password. Note that both fields are case-" -"sensitive." -msgstr "" -"Unesite tačno korisničko ime i lozinku. Pazite na razliku između malih i " -"velikih slova u oba polja." +msgid "Please enter a correct username and password. Note that both fields are case-sensitive." +msgstr "Unesite tačno korisničko ime i lozinku. Pazite na razliku između malih i velikih slova u oba polja." -#: contrib/admin/sites.py:307 contrib/admin/views/decorators.py:40 +#: contrib/admin/sites.py:307 +#: contrib/admin/views/decorators.py:40 msgid "Please log in again, because your session has expired." msgstr "Prijavite se ponovo pošto je vaša sesija istekla." -#: contrib/admin/sites.py:314 contrib/admin/views/decorators.py:47 -msgid "" -"Looks like your browser isn't configured to accept cookies. Please enable " -"cookies, reload this page, and try again." -msgstr "" -"Izgleda da vaš brauzer nije podešen da prima kolačiće. Uključite kolačiće, " -"osvežite ovu stranicu i probajte ponovo." +#: contrib/admin/sites.py:314 +#: contrib/admin/views/decorators.py:47 +msgid "Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again." +msgstr "Izgleda da vaš brauzer nije podešen da prima kolačiće. Uključite kolačiće, osvežite ovu stranicu i probajte ponovo." -#: contrib/admin/sites.py:330 contrib/admin/sites.py:336 +#: contrib/admin/sites.py:330 +#: contrib/admin/sites.py:336 #: contrib/admin/views/decorators.py:66 msgid "Usernames cannot contain the '@' character." msgstr "Korisnička imena ne smeju da sadrže znak „@“." -#: contrib/admin/sites.py:333 contrib/admin/views/decorators.py:62 +#: contrib/admin/sites.py:333 +#: contrib/admin/views/decorators.py:62 #, python-format msgid "Your e-mail address is not your username. Try '%s' instead." msgstr "Vaša imejl adresa nije vaše korisničko ime. Probajte sa „%s“." @@ -506,7 +515,8 @@ msgstr "Vaša imejl adresa nije vaše korisničko ime. Probajte sa „%s“." msgid "Site administration" msgstr "Administracija sistema" -#: contrib/admin/sites.py:403 contrib/admin/templates/admin/login.html:26 +#: contrib/admin/sites.py:403 +#: contrib/admin/templates/admin/login.html:26 #: contrib/admin/templates/registration/password_reset_complete.html:14 #: contrib/admin/views/decorators.py:20 msgid "Log in" @@ -584,12 +594,8 @@ msgid "Server Error (500)" msgstr "Greška na serveru (500)" #: contrib/admin/templates/admin/500.html:10 -msgid "" -"There's been an error. It's been reported to the site administrators via e-" -"mail and should be fixed shortly. Thanks for your patience." -msgstr "" -"Došlo je do greške. Administrator sajta je obavešten imejlom i greška će " -"biti uskoro otklonjena. Hvala na strpljenju." +msgid "There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience." +msgstr "Došlo je do greške. Administrator sajta je obavešten imejlom i greška će biti uskoro otklonjena. Hvala na strpljenju." #: contrib/admin/templates/admin/actions.html:4 msgid "Run the selected action" @@ -687,29 +693,20 @@ msgid "Filter" msgstr "Filter" #: contrib/admin/templates/admin/delete_confirmation.html:10 -#: contrib/admin/templates/admin/submit_line.html:4 forms/formsets.py:302 +#: contrib/admin/templates/admin/submit_line.html:4 +#: forms/formsets.py:300 msgid "Delete" msgstr "Obriši" #: contrib/admin/templates/admin/delete_confirmation.html:16 #, python-format -msgid "" -"Deleting the %(object_name)s '%(escaped_object)s' would result in deleting " -"related objects, but your account doesn't have permission to delete the " -"following types of objects:" -msgstr "" -"Uklanjanje %(object_name)s „%(escaped_object)s“ povlači uklanjanje svih " -"objekata koji su povezani sa ovim objektom, ali vaš nalog nema dozvole za " -"brisanje sledećih tipova objekata:" +msgid "Deleting the %(object_name)s '%(escaped_object)s' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:" +msgstr "Uklanjanje %(object_name)s „%(escaped_object)s“ povlači uklanjanje svih objekata koji su povezani sa ovim objektom, ali vaš nalog nema dozvole za brisanje sledećih tipova objekata:" #: contrib/admin/templates/admin/delete_confirmation.html:23 #, python-format -msgid "" -"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " -"All of the following related items will be deleted:" -msgstr "" -"Da sigurni da želite da obrišete %(object_name)s „%(escaped_object)s“? " -"Sledeći objekti koji su u vezi sa ovim objektom će takođe biti obrisani:" +msgid "Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? All of the following related items will be deleted:" +msgstr "Da sigurni da želite da obrišete %(object_name)s „%(escaped_object)s“? Sledeći objekti koji su u vezi sa ovim objektom će takođe biti obrisani:" #: contrib/admin/templates/admin/delete_confirmation.html:28 #: contrib/admin/templates/admin/delete_selected_confirmation.html:33 @@ -722,23 +719,13 @@ msgstr "Brisanje više objekata" #: contrib/admin/templates/admin/delete_selected_confirmation.html:15 #, python-format -msgid "" -"Deleting the %(object_name)s would result in deleting related objects, but " -"your account doesn't have permission to delete the following types of " -"objects:" -msgstr "" -"Uklanjanje %(object_name)s povlači uklanjanje svih objekata koji su povezani " -"sa ovim objektom, ali vaš nalog nema dozvole za brisanje sledećih tipova " -"objekata:" +msgid "Deleting the %(object_name)s would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:" +msgstr "Uklanjanje %(object_name)s povlači uklanjanje svih objekata koji su povezani sa ovim objektom, ali vaš nalog nema dozvole za brisanje sledećih tipova objekata:" #: contrib/admin/templates/admin/delete_selected_confirmation.html:22 #, python-format -msgid "" -"Are you sure you want to delete the selected %(object_name)s objects? All of " -"the following objects and their related items will be deleted:" -msgstr "" -"Da sigurni da želite da obrišete odabrane %(object_name)s? Sledeći objekti " -"koji su u vezi sa ovim objektom će takođe biti obrisani:" +msgid "Are you sure you want to delete the selected %(object_name)s objects? All of the following objects and their related items will be deleted:" +msgstr "Da sigurni da želite da obrišete odabrane %(object_name)s? Sledeći objekti koji su u vezi sa ovim objektom će takođe biti obrisani:" #: contrib/admin/templates/admin/filter.html:2 #, python-format @@ -775,13 +762,8 @@ msgid "Unknown content" msgstr "Nepoznat sadržaj" #: contrib/admin/templates/admin/invalid_setup.html:7 -msgid "" -"Something's wrong with your database installation. Make sure the appropriate " -"database tables have been created, and make sure the database is readable by " -"the appropriate user." -msgstr "" -"Nešto nije uredu sa vašom bazom podataka. Proverite da li postoje " -"odgovarajuće tabele i da li odgovarajući korisnik ima pristup bazi." +msgid "Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user." +msgstr "Nešto nije uredu sa vašom bazom podataka. Proverite da li postoje odgovarajuće tabele i da li odgovarajući korisnik ima pristup bazi." #: contrib/admin/templates/admin/login.html:19 msgid "Username:" @@ -804,12 +786,8 @@ msgid "Action" msgstr "Radnja" #: contrib/admin/templates/admin/object_history.html:38 -msgid "" -"This object doesn't have a change history. It probably wasn't added via this " -"admin site." -msgstr "" -"Ovaj objekat nema zabeležen istorijat izmena. Verovatno nije dodat kroz ovaj " -"sajt za administraciju." +msgid "This object doesn't have a change history. It probably wasn't added via this admin site." +msgstr "Ovaj objekat nema zabeležen istorijat izmena. Verovatno nije dodat kroz ovaj sajt za administraciju." #: contrib/admin/templates/admin/pagination.html:10 msgid "Show all" @@ -849,13 +827,13 @@ msgstr "Sačuvaj i dodaj sledeći" msgid "Save and continue editing" msgstr "Sačuvaj i nastavi sa izmenama" -#: contrib/admin/templates/admin/auth/user/add_form.html:5 -msgid "" -"First, enter a username and password. Then, you'll be able to edit more user " -"options." -msgstr "" -"Prvo unesite korisničko ime i lozinku. Potom ćete moći da menjate još " -"korisničkih podešavanja." +#: contrib/admin/templates/admin/auth/user/add_form.html:6 +msgid "First, enter a username and password. Then, you'll be able to edit more user options." +msgstr "Prvo unesite korisničko ime i lozinku. Potom ćete moći da menjate još korisničkih podešavanja." + +#: contrib/admin/templates/admin/auth/user/add_form.html:8 +msgid "Enter a username and password." +msgstr "Unesite korisničko ime i lozinku" #: contrib/admin/templates/admin/auth/user/change_password.html:28 #, python-format @@ -863,7 +841,9 @@ msgid "Enter a new password for the user %(username)s." msgstr "Unesite novu lozinku za korisnika %(username)s." #: contrib/admin/templates/admin/auth/user/change_password.html:35 -#: contrib/auth/forms.py:17 contrib/auth/forms.py:61 contrib/auth/forms.py:186 +#: contrib/auth/forms.py:17 +#: contrib/auth/forms.py:61 +#: contrib/auth/forms.py:186 msgid "Password" msgstr "Lozinka" @@ -919,12 +899,8 @@ msgid "Your password was changed." msgstr "Vaša lozinka je izmenjena." #: contrib/admin/templates/registration/password_change_form.html:21 -msgid "" -"Please enter your old password, for security's sake, and then enter your new " -"password twice so we can verify you typed it in correctly." -msgstr "" -"Iz bezbednosnih razloga prvo unesite svoju staru lozinku, a novu zatim " -"unesite dva puta da bismo mogli da proverimo da li ste je pravilno uneli." +msgid "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." +msgstr "Iz bezbednosnih razloga prvo unesite svoju staru lozinku, a novu zatim unesite dva puta da bismo mogli da proverimo da li ste je pravilno uneli." #: contrib/admin/templates/registration/password_change_form.html:27 #: contrib/auth/forms.py:170 @@ -968,12 +944,8 @@ msgid "Enter new password" msgstr "Unesite novu lozinku" #: contrib/admin/templates/registration/password_reset_confirm.html:14 -msgid "" -"Please enter your new password twice so we can verify you typed it in " -"correctly." -msgstr "" -"Unesite novu lozinku dva puta kako bismo mogli da proverimo da li ste je " -"pravilno uneli." +msgid "Please enter your new password twice so we can verify you typed it in correctly." +msgstr "Unesite novu lozinku dva puta kako bismo mogli da proverimo da li ste je pravilno uneli." #: contrib/admin/templates/registration/password_reset_confirm.html:18 msgid "New password:" @@ -988,12 +960,8 @@ msgid "Password reset unsuccessful" msgstr "Resetovanje lozinke neuspešno" #: contrib/admin/templates/registration/password_reset_confirm.html:28 -msgid "" -"The password reset link was invalid, possibly because it has already been " -"used. Please request a new password reset." -msgstr "" -"Link za resetovanje lozinke nije važeći, verovatno zato što je već " -"iskorišćen. Ponovo zatražite resetovanje lozinke." +msgid "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." +msgstr "Link za resetovanje lozinke nije važeći, verovatno zato što je već iskorišćen. Ponovo zatražite resetovanje lozinke." #: contrib/admin/templates/registration/password_reset_done.html:6 #: contrib/admin/templates/registration/password_reset_done.html:10 @@ -1001,12 +969,8 @@ msgid "Password reset successful" msgstr "Resetovanje lozinke uspešno." #: contrib/admin/templates/registration/password_reset_done.html:12 -msgid "" -"We've e-mailed you instructions for setting your password to the e-mail " -"address you submitted. You should be receiving it shortly." -msgstr "" -"Poslali smo uputstva za postavljanje nove lozinke na imejl adresu koju ste " -"nam dali. Uputstva ćete dobiti uskoro." +msgid "We've e-mailed you instructions for setting your password to the e-mail address you submitted. You should be receiving it shortly." +msgstr "Poslali smo uputstva za postavljanje nove lozinke na imejl adresu koju ste nam dali. Uputstva ćete dobiti uskoro." #: contrib/admin/templates/registration/password_reset_email.html:2 msgid "You're receiving this e-mail because you requested a password reset" @@ -1035,12 +999,8 @@ msgid "The %(site_name)s team" msgstr "Ekipa sajta %(site_name)s" #: contrib/admin/templates/registration/password_reset_form.html:12 -msgid "" -"Forgotten your password? Enter your e-mail address below, and we'll e-mail " -"instructions for setting a new one." -msgstr "" -"Zaboravili ste lozinku? Unesite svoju imejl adresu dole i poslaćemo vam " -"uputstva za postavljanje nove." +msgid "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." +msgstr "Zaboravili ste lozinku? Unesite svoju imejl adresu dole i poslaćemo vam uputstva za postavljanje nove." #: contrib/admin/templates/registration/password_reset_form.html:16 msgid "E-mail address:" @@ -1050,7 +1010,7 @@ msgstr "Imejl adresa:" msgid "Reset my password" msgstr "Resetuj moju lozinku" -#: contrib/admin/templatetags/admin_list.py:239 +#: contrib/admin/templatetags/admin_list.py:257 msgid "All dates" msgstr "Svi datumi" @@ -1064,7 +1024,8 @@ msgstr "Odaberi objekat klase %s" msgid "Select %s to change" msgstr "Odaberi objekat klase %s za izmenu" -#: contrib/admin/views/template.py:38 contrib/sites/models.py:38 +#: contrib/admin/views/template.py:38 +#: contrib/sites/models.py:38 msgid "site" msgstr "sajt" @@ -1072,17 +1033,20 @@ msgstr "sajt" msgid "template" msgstr "templejt" -#: contrib/admindocs/views.py:61 contrib/admindocs/views.py:63 +#: contrib/admindocs/views.py:61 +#: contrib/admindocs/views.py:63 #: contrib/admindocs/views.py:65 msgid "tag:" msgstr "tag:" -#: contrib/admindocs/views.py:94 contrib/admindocs/views.py:96 +#: contrib/admindocs/views.py:94 +#: contrib/admindocs/views.py:96 #: contrib/admindocs/views.py:98 msgid "filter:" msgstr "filter:" -#: contrib/admindocs/views.py:158 contrib/admindocs/views.py:160 +#: contrib/admindocs/views.py:158 +#: contrib/admindocs/views.py:160 #: contrib/admindocs/views.py:162 msgid "view:" msgstr "vju:" @@ -1102,28 +1066,34 @@ msgstr "Model %(model_name)r nije pronađen u aplikaciji %(app_label)r" msgid "the related `%(app_label)s.%(data_type)s` object" msgstr "povezani objekti klase `%(app_label)s.%(data_type)s`" -#: contrib/admindocs/views.py:209 contrib/admindocs/views.py:228 -#: contrib/admindocs/views.py:233 contrib/admindocs/views.py:247 -#: contrib/admindocs/views.py:261 contrib/admindocs/views.py:266 +#: contrib/admindocs/views.py:209 +#: contrib/admindocs/views.py:228 +#: contrib/admindocs/views.py:233 +#: contrib/admindocs/views.py:247 +#: contrib/admindocs/views.py:261 +#: contrib/admindocs/views.py:266 msgid "model:" msgstr "model:" # WARN: possible breakage in future # This string is interpolated in strings below, which can cause breakage in # future releases. -#: contrib/admindocs/views.py:224 contrib/admindocs/views.py:256 +#: contrib/admindocs/views.py:224 +#: contrib/admindocs/views.py:256 #, python-format msgid "related `%(app_label)s.%(object_name)s` objects" msgstr "klase `%(app_label)s.%(object_name)s`" # WARN: possible breakage in future -#: contrib/admindocs/views.py:228 contrib/admindocs/views.py:261 +#: contrib/admindocs/views.py:228 +#: contrib/admindocs/views.py:261 #, python-format msgid "all %s" msgstr "svi povezani objekti %s" # WARN: possible breakage in future -#: contrib/admindocs/views.py:233 contrib/admindocs/views.py:266 +#: contrib/admindocs/views.py:233 +#: contrib/admindocs/views.py:266 #, python-format msgid "number of %s" msgstr "broj povezanih objekata %s" @@ -1170,24 +1140,16 @@ msgid "Documentation for this page" msgstr "Dokumentacija za ovu stranicu" #: contrib/admindocs/templates/admin_doc/bookmarklets.html:19 -msgid "" -"Jumps you from any page to the documentation for the view that generates " -"that page." -msgstr "" -"Vodi od bilo koje stranice do dokumentaicje pogleda koji je generisao tu " -"stranicu." +msgid "Jumps you from any page to the documentation for the view that generates that page." +msgstr "Vodi od bilo koje stranice do dokumentaicje pogleda koji je generisao tu stranicu." #: contrib/admindocs/templates/admin_doc/bookmarklets.html:21 msgid "Show object ID" msgstr "Prikaži ID objekta" #: contrib/admindocs/templates/admin_doc/bookmarklets.html:22 -msgid "" -"Shows the content-type and unique ID for pages that represent a single " -"object." -msgstr "" -"Prikazuje content-type i jedinstveni ID za stranicu koja prestavlja jedan " -"objekat." +msgid "Shows the content-type and unique ID for pages that represent a single object." +msgstr "Prikazuje content-type i jedinstveni ID za stranicu koja prestavlja jedan objekat." #: contrib/admindocs/templates/admin_doc/bookmarklets.html:24 msgid "Edit this object (current window)" @@ -1195,8 +1157,7 @@ msgstr "Izmeni ovaj objekat (u ovom prozoru)" #: contrib/admindocs/templates/admin_doc/bookmarklets.html:25 msgid "Jumps to the admin page for pages that represent a single object." -msgstr "" -"Vodi u administracioni stranicu za stranice koje prestavljaju jedan objekat" +msgstr "Vodi u administracioni stranicu za stranice koje prestavljaju jedan objekat" #: contrib/admindocs/templates/admin_doc/bookmarklets.html:27 msgid "Edit this object (new window)" @@ -1204,8 +1165,7 @@ msgstr "Izmeni ovaj objekat (novi prozor)" #: contrib/admindocs/templates/admin_doc/bookmarklets.html:28 msgid "As above, but opens the admin page in a new window." -msgstr "" -"Isto kao prethodni, ali otvara administracionu stranicu u novom prozoru." +msgstr "Isto kao prethodni, ali otvara administracionu stranicu u novom prozoru." #: contrib/auth/admin.py:29 msgid "Personal info" @@ -1232,17 +1192,19 @@ msgstr "Lozinka uspešno izmenjena." msgid "Change password: %s" msgstr "Izmeni lozinku: %s" -#: contrib/auth/forms.py:14 contrib/auth/forms.py:48 contrib/auth/forms.py:60 +#: contrib/auth/forms.py:14 +#: contrib/auth/forms.py:48 +#: contrib/auth/forms.py:60 msgid "Username" msgstr "Korisnik" -#: contrib/auth/forms.py:15 contrib/auth/forms.py:49 +#: contrib/auth/forms.py:15 +#: contrib/auth/forms.py:49 msgid "Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only." -msgstr "" -"Neophodno. Najviše 30 slovnih mesta. Samo alfanumerički znaci (slova, brojke " -"i @/./+/-/_)." +msgstr "Neophodno. Najviše 30 slovnih mesta. Samo alfanumerički znaci (slova, brojke i @/./+/-/_)." -#: contrib/auth/forms.py:16 contrib/auth/forms.py:50 +#: contrib/auth/forms.py:16 +#: contrib/auth/forms.py:50 msgid "This value may contain only letters, numbers and @/./+/-/_ characters." msgstr "Ova vrednost može sadržati samo slova, brojke i @/./+/-/_." @@ -1254,7 +1216,8 @@ msgstr "Potvrda lozinke" msgid "A user with that username already exists." msgstr "Korisnik sa tim korisničkim imenom već postoji." -#: contrib/auth/forms.py:37 contrib/auth/forms.py:156 +#: contrib/auth/forms.py:37 +#: contrib/auth/forms.py:156 #: contrib/auth/forms.py:198 msgid "The two password fields didn't match." msgstr "Dva polja za lozinke se nisu poklopila." @@ -1264,24 +1227,16 @@ msgid "This account is inactive." msgstr "Ovaj nalog je neaktivan." #: contrib/auth/forms.py:88 -msgid "" -"Your Web browser doesn't appear to have cookies enabled. Cookies are " -"required for logging in." -msgstr "" -"Izgleda da su kolačići isključeni u vašem brauzeru. Oni moraju biti " -"uključeni da bi ste se prijavili." +msgid "Your Web browser doesn't appear to have cookies enabled. Cookies are required for logging in." +msgstr "Izgleda da su kolačići isključeni u vašem brauzeru. Oni moraju biti uključeni da bi ste se prijavili." #: contrib/auth/forms.py:101 msgid "E-mail" msgstr "Imejl adresa" #: contrib/auth/forms.py:110 -msgid "" -"That e-mail address doesn't have an associated user account. Are you sure " -"you've registered?" -msgstr "" -"Ta imejl adresa nije u vezi ni sa jednim nalogom. Da li ste sigurni da ste " -"se već registrovali?" +msgid "That e-mail address doesn't have an associated user account. Are you sure you've registered?" +msgstr "Ta imejl adresa nije u vezi ni sa jednim nalogom. Da li ste sigurni da ste se već registrovali?" #: contrib/auth/forms.py:136 #, python-format @@ -1296,7 +1251,8 @@ msgstr "Potvrda nove lozinke" msgid "Your old password was entered incorrectly. Please enter it again." msgstr "Vaša stara loznka nije pravilno unesena. Unesite je ponovo." -#: contrib/auth/models.py:66 contrib/auth/models.py:94 +#: contrib/auth/models.py:66 +#: contrib/auth/models.py:94 msgid "name" msgstr "ime" @@ -1308,7 +1264,8 @@ msgstr "šifra dozvole" msgid "permission" msgstr "dozvola" -#: contrib/auth/models.py:73 contrib/auth/models.py:95 +#: contrib/auth/models.py:73 +#: contrib/auth/models.py:95 msgid "permissions" msgstr "dozvole" @@ -1316,7 +1273,8 @@ msgstr "dozvole" msgid "group" msgstr "grupa" -#: contrib/auth/models.py:99 contrib/auth/models.py:206 +#: contrib/auth/models.py:99 +#: contrib/auth/models.py:206 msgid "groups" msgstr "grupe" @@ -1325,11 +1283,8 @@ msgid "username" msgstr "korisničko ime" #: contrib/auth/models.py:196 -msgid "" -"Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters" -msgstr "" -"Neophodno. Najviše 30 slovnih mesta. Samo alfanumerički znaci (slova, brojke " -"i @/./+/-/_)." +msgid "Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters" +msgstr "Neophodno. Najviše 30 slovnih mesta. Samo alfanumerički znaci (slova, brojke i @/./+/-/_)." #: contrib/auth/models.py:197 msgid "first name" @@ -1348,12 +1303,8 @@ msgid "password" msgstr "lozinka" #: contrib/auth/models.py:200 -msgid "" -"Use '[algo]$[salt]$[hexdigest]' or use the change " -"password form." -msgstr "" -"Koristite '[algo]$[salt]$[hexdigest]' ili formular za " -"unos lozinke." +msgid "Use '[algo]$[salt]$[hexdigest]' or use the change password form." +msgstr "Koristite '[algo]$[salt]$[hexdigest]' ili formular za unos lozinke." #: contrib/auth/models.py:201 msgid "staff status" @@ -1361,32 +1312,23 @@ msgstr "status člana posade" #: contrib/auth/models.py:201 msgid "Designates whether the user can log into this admin site." -msgstr "" -"Označava da li korisnik može da se prijavi na ovaj sajt za administraciju." +msgstr "Označava da li korisnik može da se prijavi na ovaj sajt za administraciju." #: contrib/auth/models.py:202 msgid "active" msgstr "aktivan" #: contrib/auth/models.py:202 -msgid "" -"Designates whether this user should be treated as active. Unselect this " -"instead of deleting accounts." -msgstr "" -"Označava da li se korisnik smatra aktivnim. Deselektujte ovo umesto da " -"brišete nalog." +msgid "Designates whether this user should be treated as active. Unselect this instead of deleting accounts." +msgstr "Označava da li se korisnik smatra aktivnim. Deselektujte ovo umesto da brišete nalog." #: contrib/auth/models.py:203 msgid "superuser status" msgstr "status administratora" #: contrib/auth/models.py:203 -msgid "" -"Designates that this user has all permissions without explicitly assigning " -"them." -msgstr "" -"Označava da li korisnik ima sve dozvole bez dodeljivanja pojedinačnih " -"dozvola." +msgid "Designates that this user has all permissions without explicitly assigning them." +msgstr "Označava da li korisnik ima sve dozvole bez dodeljivanja pojedinačnih dozvola." #: contrib/auth/models.py:204 msgid "last login" @@ -1397,18 +1339,15 @@ msgid "date joined" msgstr "datum registracije" #: contrib/auth/models.py:207 -msgid "" -"In addition to the permissions manually assigned, this user will also get " -"all permissions granted to each group he/she is in." -msgstr "" -"Pored ručno dodeljenih dozvola, ovaj korisnik će imati i dozvole dodeljene " -"gurpama kojima pripada." +msgid "In addition to the permissions manually assigned, this user will also get all permissions granted to each group he/she is in." +msgstr "Pored ručno dodeljenih dozvola, ovaj korisnik će imati i dozvole dodeljene gurpama kojima pripada." #: contrib/auth/models.py:208 msgid "user permissions" msgstr "korisničke dozvole" -#: contrib/auth/models.py:212 contrib/comments/models.py:50 +#: contrib/auth/models.py:212 +#: contrib/comments/models.py:50 #: contrib/comments/models.py:168 msgid "user" msgstr "korisnik" @@ -1425,8 +1364,9 @@ msgstr "poruka" msgid "Logged out" msgstr "Odjavljen" -#: contrib/auth/management/commands/createsuperuser.py:23 -#: core/validators.py:120 forms/fields.py:428 +#: contrib/auth/management/commands/createsuperuser.py:24 +#: core/validators.py:120 +#: forms/fields.py:427 msgid "Enter a valid e-mail address." msgstr "Unesite važeću imejl adresu." @@ -1497,8 +1437,10 @@ msgstr "Ime" msgid "Email address" msgstr "Imejl adresa" -#: contrib/comments/forms.py:95 contrib/flatpages/admin.py:8 -#: contrib/flatpages/models.py:7 db/models/fields/__init__.py:1101 +#: contrib/comments/forms.py:95 +#: contrib/flatpages/admin.py:8 +#: contrib/flatpages/models.py:7 +#: db/models/fields/__init__.py:1109 msgid "URL" msgstr "URL" @@ -1515,11 +1457,11 @@ msgstr[1] "Pazi na jezik! Reči „%s“ ovde nisu dozvoljene." msgstr[2] "Pazi na jezik! Reči „%s“ ovde nisu dozvoljene." #: contrib/comments/forms.py:182 -msgid "" -"If you enter anything in this field your comment will be treated as spam" +msgid "If you enter anything in this field your comment will be treated as spam" msgstr "Ako išta unesete u ovo polje, Vaš komentar će se smatrati spamom." -#: contrib/comments/models.py:22 contrib/contenttypes/models.py:81 +#: contrib/comments/models.py:22 +#: contrib/contenttypes/models.py:81 msgid "content type" msgstr "tip sadržaja" @@ -1539,7 +1481,8 @@ msgstr "korisnikova imejl adresa" msgid "user's URL" msgstr "korisnikov URL" -#: contrib/comments/models.py:56 contrib/comments/models.py:76 +#: contrib/comments/models.py:56 +#: contrib/comments/models.py:76 #: contrib/comments/models.py:169 msgid "comment" msgstr "komentar" @@ -1548,7 +1491,8 @@ msgstr "komentar" msgid "date/time submitted" msgstr "datum/vreme postavljanja" -#: contrib/comments/models.py:60 db/models/fields/__init__.py:896 +#: contrib/comments/models.py:60 +#: db/models/fields/__init__.py:904 msgid "IP address" msgstr "IP adresa" @@ -1557,42 +1501,28 @@ msgid "is public" msgstr "javno" #: contrib/comments/models.py:62 -msgid "" -"Uncheck this box to make the comment effectively disappear from the site." -msgstr "" -"Deselektujte ovo polje ako želite da poruka faktički nestane sa ovog sajta." +msgid "Uncheck this box to make the comment effectively disappear from the site." +msgstr "Deselektujte ovo polje ako želite da poruka faktički nestane sa ovog sajta." #: contrib/comments/models.py:64 msgid "is removed" msgstr "uklonjen" #: contrib/comments/models.py:65 -msgid "" -"Check this box if the comment is inappropriate. A \"This comment has been " -"removed\" message will be displayed instead." -msgstr "" -"Obeležite ovu kućicu ako je komentar neprikladan. Poruka o uklanjanju će " -"biti prikazana umesto komentara." +msgid "Check this box if the comment is inappropriate. A \"This comment has been removed\" message will be displayed instead." +msgstr "Obeležite ovu kućicu ako je komentar neprikladan. Poruka o uklanjanju će biti prikazana umesto komentara." #: contrib/comments/models.py:77 msgid "comments" msgstr "komentari" #: contrib/comments/models.py:119 -msgid "" -"This comment was posted by an authenticated user and thus the name is read-" -"only." -msgstr "" -"Ovaj komentar je postavio prijavljen korisnik i zato je polje sa imenom " -"zaključano." +msgid "This comment was posted by an authenticated user and thus the name is read-only." +msgstr "Ovaj komentar je postavio prijavljen korisnik i zato je polje sa imenom zaključano." #: contrib/comments/models.py:128 -msgid "" -"This comment was posted by an authenticated user and thus the email is read-" -"only." -msgstr "" -"Ovaj komentar je postavio prijavljen korisnik i zato je polje sa imejl " -"adresom zaključano." +msgid "This comment was posted by an authenticated user and thus the email is read-only." +msgstr "Ovaj komentar je postavio prijavljen korisnik i zato je polje sa imejl adresom zaključano." #: contrib/comments/models.py:153 #, python-format @@ -1644,8 +1574,7 @@ msgstr "Hvala na odobrenju!" #: contrib/comments/templates/comments/approved.html:7 #: contrib/comments/templates/comments/deleted.html:7 #: contrib/comments/templates/comments/flagged.html:7 -msgid "" -"Thanks for taking the time to improve the quality of discussion on our site" +msgid "Thanks for taking the time to improve the quality of discussion on our site" msgstr "Hvala na učešću u unapređenju kvaliteta diskusija na našem sajtu." #: contrib/comments/templates/comments/delete.html:4 @@ -1723,19 +1652,12 @@ msgid "content types" msgstr "tipovi sadržaja" #: contrib/flatpages/admin.py:9 -msgid "" -"Example: '/about/contact/'. Make sure to have leading and trailing slashes." -msgstr "" -"Primer: '/about/contact/'. Pazite na to da postoje i početne i završne kose " -"crte." +msgid "Example: '/about/contact/'. Make sure to have leading and trailing slashes." +msgstr "Primer: '/about/contact/'. Pazite na to da postoje i početne i završne kose crte." #: contrib/flatpages/admin.py:11 -msgid "" -"This value must contain only letters, numbers, underscores, dashes or " -"slashes." -msgstr "" -"Ova vrednost može sadržati samo slova, brojke, donje crte, crtice ili kose " -"crte." +msgid "This value must contain only letters, numbers, underscores, dashes or slashes." +msgstr "Ova vrednost može sadržati samo slova, brojke, donje crte, crtice ili kose crte." #: contrib/flatpages/admin.py:22 msgid "Advanced options" @@ -1758,12 +1680,8 @@ msgid "template name" msgstr "naziv templejta" #: contrib/flatpages/models.py:12 -msgid "" -"Example: 'flatpages/contact_page.html'. If this isn't provided, the system " -"will use 'flatpages/default.html'." -msgstr "" -"Primer: 'flatpages/contact_page.html'. Ako ovo ostavite praznim, sistem će " -"koristiti 'flatpages/default.html'." +msgid "Example: 'flatpages/contact_page.html'. If this isn't provided, the system will use 'flatpages/default.html'." +msgstr "Primer: 'flatpages/contact_page.html'. Ako ovo ostavite praznim, sistem će koristiti 'flatpages/default.html'." #: contrib/flatpages/models.py:13 msgid "registration required" @@ -1771,9 +1689,7 @@ msgstr "potrebna registracija" #: contrib/flatpages/models.py:13 msgid "If this is checked, only logged-in users will be able to view the page." -msgstr "" -"Ako je ovo obeleženo, samo će prijavljeni korisnici moći da vide ovu " -"stranicu." +msgstr "Ako je ovo obeleženo, samo će prijavljeni korisnici moći da vide ovu stranicu." #: contrib/flatpages/models.py:18 msgid "flat page" @@ -1784,17 +1700,12 @@ msgid "flat pages" msgstr "flet stranice" #: contrib/formtools/wizard.py:140 -msgid "" -"We apologize, but your form has expired. Please continue filling out the " -"form from this page." -msgstr "" -"Žao nam je, ali Vaša sesija je istekla. Popunjavanje formulara nastavite na " -"ovoj stranici." +msgid "We apologize, but your form has expired. Please continue filling out the form from this page." +msgstr "Žao nam je, ali Vaša sesija je istekla. Popunjavanje formulara nastavite na ovoj stranici." #: contrib/gis/db/models/fields.py:50 msgid "The base GIS field -- maps to the OpenGIS Specification Geometry type." -msgstr "" -"Osnovno „GIS“ polje koje mapira tip geometrije po „OpenGIS“ specifikaciji." +msgstr "Osnovno „GIS“ polje koje mapira tip geometrije po „OpenGIS“ specifikaciji." #: contrib/gis/db/models/fields.py:270 msgid "Point" @@ -1837,9 +1748,7 @@ msgid "Invalid geometry type." msgstr "Nepostojeći tip geometrije." #: contrib/gis/forms/fields.py:20 -msgid "" -"An error occurred when transforming the geometry to the SRID of the geometry " -"form field." +msgid "An error occurred when transforming the geometry to the SRID of the geometry form field." msgstr "Greška se desila tokom transformacije geometrije na „SRID“ tip polja." #: contrib/humanize/templatetags/humanize.py:19 @@ -1934,8 +1843,10 @@ msgstr "juče" msgid "Enter a postal code in the format NNNN or ANNNNAAA." msgstr "Unesite poštanski broj u formatu NNNN ili ANNNNAAA." -#: contrib/localflavor/ar/forms.py:50 contrib/localflavor/br/forms.py:92 -#: contrib/localflavor/br/forms.py:131 contrib/localflavor/pe/forms.py:24 +#: contrib/localflavor/ar/forms.py:50 +#: contrib/localflavor/br/forms.py:92 +#: contrib/localflavor/br/forms.py:131 +#: contrib/localflavor/pe/forms.py:24 #: contrib/localflavor/pe/forms.py:52 msgid "This field requires only numbers." msgstr "Ovo polje mora da sadrži samo brojke." @@ -1988,15 +1899,15 @@ msgstr "Voralber" msgid "Vienna" msgstr "Beč" -#: contrib/localflavor/at/forms.py:20 contrib/localflavor/ch/forms.py:17 +#: contrib/localflavor/at/forms.py:20 +#: contrib/localflavor/ch/forms.py:17 #: contrib/localflavor/no/forms.py:13 msgid "Enter a zip code in the format XXXX." msgstr "Unesite poštanski broj u formatu XXXX." #: contrib/localflavor/at/forms.py:48 msgid "Enter a valid Austrian Social Security Number in XXXX XXXXXX format." -msgstr "" -"Unesite važeći austrijski broj socijalnog osiguranja u formatu XXXX XXXXXX." +msgstr "Unesite važeći austrijski broj socijalnog osiguranja u formatu XXXX XXXXXX." #: contrib/localflavor/au/forms.py:17 msgid "Enter a 4 digit post code." @@ -2011,9 +1922,7 @@ msgid "Phone numbers must be in XX-XXXX-XXXX format." msgstr "Broj telefona mora biti u formatu XX-XXXX-XXXX." #: contrib/localflavor/br/forms.py:54 -msgid "" -"Select a valid brazilian state. That state is not one of the available " -"states." +msgid "Select a valid brazilian state. That state is not one of the available states." msgstr "Odaberite postojeću brazilsku državu. Ta država nije među ponuđenima." #: contrib/localflavor/br/forms.py:90 @@ -2145,9 +2054,7 @@ msgid "Zurich" msgstr "" #: contrib/localflavor/ch/forms.py:65 -msgid "" -"Enter a valid Swiss identity or passport card number in X1234567<0 or " -"1234567890 format." +msgid "Enter a valid Swiss identity or passport card number in X1234567<0 or 1234567890 format." msgstr "" #: contrib/localflavor/cl/forms.py:30 @@ -2218,7 +2125,8 @@ msgstr "" msgid "Moravian-Silesian Region" msgstr "" -#: contrib/localflavor/cz/forms.py:28 contrib/localflavor/sk/forms.py:30 +#: contrib/localflavor/cz/forms.py:28 +#: contrib/localflavor/sk/forms.py:30 msgid "Enter a postal code in the format XXXXX or XXX XX." msgstr "" @@ -2302,15 +2210,14 @@ msgstr "" msgid "Thuringia" msgstr "" -#: contrib/localflavor/de/forms.py:15 contrib/localflavor/fi/forms.py:13 +#: contrib/localflavor/de/forms.py:15 +#: contrib/localflavor/fi/forms.py:13 #: contrib/localflavor/fr/forms.py:16 msgid "Enter a zip code in the format XXXXX." msgstr "" #: contrib/localflavor/de/forms.py:42 -msgid "" -"Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X " -"format." +msgid "Enter a valid German identity card number in XXXXXXXXXXX-XXXXXXX-XXXXXXX-X format." msgstr "" #: contrib/localflavor/es/es_provinces.py:5 @@ -2585,9 +2492,7 @@ msgid "Enter a valid postal code in the range and format 01XXX - 52XXX." msgstr "" #: contrib/localflavor/es/forms.py:40 -msgid "" -"Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or " -"9XXXXXXXX." +msgid "Enter a valid phone number in one of the formats 6XXXXXXXX, 8XXXXXXXX or 9XXXXXXXX." msgstr "" #: contrib/localflavor/es/forms.py:67 @@ -2611,8 +2516,7 @@ msgid "Invalid checksum for CIF." msgstr "" #: contrib/localflavor/es/forms.py:143 -msgid "" -"Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX." +msgid "Please enter a valid bank account number in format XXXX-XXXX-XX-XXXXXXXXXX." msgstr "" #: contrib/localflavor/es/forms.py:144 @@ -2631,7 +2535,8 @@ msgstr "" msgid "Enter a valid post code" msgstr "" -#: contrib/localflavor/id/forms.py:68 contrib/localflavor/nl/forms.py:53 +#: contrib/localflavor/id/forms.py:68 +#: contrib/localflavor/nl/forms.py:53 msgid "Enter a valid phone number" msgstr "" @@ -3060,8 +2965,7 @@ msgid "Enter a zip code in the format XXXXXXX." msgstr "" #: contrib/localflavor/is_/forms.py:18 -msgid "" -"Enter a valid Icelandic identification number. The format is XXXXXX-XXXX." +msgid "Enter a valid Icelandic identification number. The format is XXXXXX-XXXX." msgstr "" #: contrib/localflavor/is_/forms.py:19 @@ -3481,8 +3385,7 @@ msgid "Wrong checksum for the National Identification Number." msgstr "" #: contrib/localflavor/pl/forms.py:71 -msgid "" -"Enter a tax number field (NIP) in the format XXX-XXX-XX-XX or XX-XX-XXX-XXX." +msgid "Enter a tax number field (NIP) in the format XXX-XXX-XX-XX or XX-XX-XXX-XXX." msgstr "" #: contrib/localflavor/pl/forms.py:72 @@ -4410,24 +4313,16 @@ msgid "redirect from" msgstr "preusmeren sa" #: contrib/redirects/models.py:8 -msgid "" -"This should be an absolute path, excluding the domain name. Example: '/" -"events/search/'." -msgstr "" -"Ovo mora biti apsolutna putanja bez imena domena. Na primer: '/events/" -"search/'." +msgid "This should be an absolute path, excluding the domain name. Example: '/events/search/'." +msgstr "Ovo mora biti apsolutna putanja bez imena domena. Na primer: '/events/search/'." #: contrib/redirects/models.py:9 msgid "redirect to" msgstr "preusmeri ka" #: contrib/redirects/models.py:10 -msgid "" -"This can be either an absolute path (as above) or a full URL starting with " -"'http://'." -msgstr "" -"Ovo može biti ili apsolutna putanja (kao gore) ili pun URL koji počinje sa " -"'http://'." +msgid "This can be either an absolute path (as above) or a full URL starting with 'http://'." +msgstr "Ovo može biti ili apsolutna putanja (kao gore) ili pun URL koji počinje sa 'http://'." #: contrib/redirects/models.py:13 msgid "redirect" @@ -4469,30 +4364,33 @@ msgstr "prikazano ime" msgid "sites" msgstr "sajtovi" -#: core/validators.py:20 forms/fields.py:66 +#: core/validators.py:20 +#: forms/fields.py:66 msgid "Enter a valid value." msgstr "Unesite ispravnu vrednost." -#: core/validators.py:87 forms/fields.py:529 +#: core/validators.py:87 +#: forms/fields.py:528 msgid "Enter a valid URL." msgstr "Unesite ispravan URL." -#: core/validators.py:89 forms/fields.py:530 +#: core/validators.py:89 +#: forms/fields.py:529 msgid "This URL appears to be a broken link." msgstr "Ovaj URL izgleda ne vodi nikuda." -#: core/validators.py:123 forms/fields.py:873 -msgid "" -"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." -msgstr "" -"Unesite isrpavan „slag“, koji se sastoji od slova, brojki, donjih crta ili " -"cirtica." +#: core/validators.py:123 +#: forms/fields.py:877 +msgid "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "Unesite isrpavan „slag“, koji se sastoji od slova, brojki, donjih crta ili cirtica." -#: core/validators.py:126 forms/fields.py:866 +#: core/validators.py:126 +#: forms/fields.py:870 msgid "Enter a valid IPv4 address." msgstr "Unesite ispravnu IPv4 adresu." -#: core/validators.py:129 db/models/fields/__init__.py:572 +#: core/validators.py:129 +#: db/models/fields/__init__.py:572 msgid "Enter only digits separated by commas." msgstr "Unesite samo brojke razdvojene zapetama." @@ -4501,40 +4399,37 @@ msgstr "Unesite samo brojke razdvojene zapetama." msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." msgstr "Ovo polje mora da bude %(limit_value)s (trenutno ima %(show_value)s)." -#: core/validators.py:153 forms/fields.py:205 forms/fields.py:257 +#: core/validators.py:153 +#: forms/fields.py:204 +#: forms/fields.py:256 #, python-format msgid "Ensure this value is less than or equal to %(limit_value)s." msgstr "Ova vrednost mora da bude manja od %(limit_value)s. ili tačno toliko." -#: core/validators.py:158 forms/fields.py:206 forms/fields.py:258 +#: core/validators.py:158 +#: forms/fields.py:205 +#: forms/fields.py:257 #, python-format msgid "Ensure this value is greater than or equal to %(limit_value)s." msgstr "Ova vrednost mora biti veća od %(limit_value)s ili tačno toliko." #: core/validators.py:164 #, python-format -msgid "" -"Ensure this value has at least %(limit_value)d characters (it has %" -"(show_value)d)." -msgstr "" -"Ovo polje mora da sadrži najmanje %(limit_value)d slovnih mesta (trenutno " -"ima %(show_value)d)." +msgid "Ensure this value has at least %(limit_value)d characters (it has %(show_value)d)." +msgstr "Ovo polje mora da sadrži najmanje %(limit_value)d slovnih mesta (trenutno ima %(show_value)d)." #: core/validators.py:170 #, python-format -msgid "" -"Ensure this value has at most %(limit_value)d characters (it has %" -"(show_value)d)." -msgstr "" -"Ovo polje mora da sadrži najviše %(limit_value)d slovnih mesta (trenutno ima " -"%(show_value)d)." +msgid "Ensure this value has at most %(limit_value)d characters (it has %(show_value)d)." +msgstr "Ovo polje mora da sadrži najviše %(limit_value)d slovnih mesta (trenutno ima %(show_value)d)." -#: db/models/base.py:822 +#: db/models/base.py:823 #, python-format msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "%(field_name)s mora da bude jedinstven za %(date_field)s %(lookup)s." -#: db/models/base.py:837 db/models/base.py:845 +#: db/models/base.py:838 +#: db/models/base.py:846 #, python-format msgid "%(model_name)s with this %(field_label)s already exists." msgstr "%(model_name)s sa ovom vrednošću %(field_label)s već postoji." @@ -4557,13 +4452,16 @@ msgstr "Ovo polje ne može da ostane prazno." msgid "Field of type: %(field_type)s" msgstr "Ponje tipa: %(field_type)s" -#: db/models/fields/__init__.py:451 db/models/fields/__init__.py:852 -#: db/models/fields/__init__.py:961 db/models/fields/__init__.py:972 -#: db/models/fields/__init__.py:999 +#: db/models/fields/__init__.py:451 +#: db/models/fields/__init__.py:860 +#: db/models/fields/__init__.py:969 +#: db/models/fields/__init__.py:980 +#: db/models/fields/__init__.py:1007 msgid "Integer" msgstr "Ceo broj" -#: db/models/fields/__init__.py:455 db/models/fields/__init__.py:850 +#: db/models/fields/__init__.py:455 +#: db/models/fields/__init__.py:858 msgid "This value must be an integer." msgstr "Ova vrednost mora biti celobrojna." @@ -4575,7 +4473,8 @@ msgstr "Ova vrednost mora biti True ili False." msgid "Boolean (Either True or False)" msgstr "Bulova vrednost (True ili False)" -#: db/models/fields/__init__.py:539 db/models/fields/__init__.py:982 +#: db/models/fields/__init__.py:539 +#: db/models/fields/__init__.py:990 #, python-format msgid "String (up to %(max_length)s)" msgstr "String (najviše %(max_length)s znakova)" @@ -4617,44 +4516,45 @@ msgstr "Decimalni broj" msgid "E-mail address" msgstr "Imejl adresa" -#: db/models/fields/__init__.py:799 db/models/fields/files.py:220 +#: db/models/fields/__init__.py:807 +#: db/models/fields/files.py:220 #: db/models/fields/files.py:331 msgid "File path" msgstr "Putanja fajla" -#: db/models/fields/__init__.py:822 +#: db/models/fields/__init__.py:830 msgid "This value must be a float." msgstr "Ova vrednost mora biti broj sa klizećom zapetom" -#: db/models/fields/__init__.py:824 +#: db/models/fields/__init__.py:832 msgid "Floating point number" msgstr "Broj sa pokrenom zapetom" -#: db/models/fields/__init__.py:883 +#: db/models/fields/__init__.py:891 msgid "Big (8 byte) integer" msgstr "Veliki ceo broj" -#: db/models/fields/__init__.py:912 +#: db/models/fields/__init__.py:920 msgid "This value must be either None, True or False." msgstr "Ova vrednost mora biti ili None, ili True, ili False." -#: db/models/fields/__init__.py:914 +#: db/models/fields/__init__.py:922 msgid "Boolean (Either True, False or None)" msgstr "Bulova vrednost (True, False ili None)" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1013 msgid "Text" msgstr "Tekst" -#: db/models/fields/__init__.py:1021 +#: db/models/fields/__init__.py:1029 msgid "Time" msgstr "Vreme" -#: db/models/fields/__init__.py:1025 +#: db/models/fields/__init__.py:1033 msgid "Enter a valid time in HH:MM[:ss[.uuuuuu]] format." msgstr "Unesite ispravno vreme u formatu ČČ:MM[:ss[.uuuuuu]]." -#: db/models/fields/__init__.py:1109 +#: db/models/fields/__init__.py:1125 msgid "XML text" msgstr "XML tekst" @@ -4667,26 +4567,22 @@ msgstr "Objekat klase %(model)s sa primarnim ključem %(pk)r ne postoji." msgid "Foreign Key (type determined by related field)" msgstr "Strani ključ (tip određuje referentno polje)" -#: db/models/fields/related.py:918 +#: db/models/fields/related.py:919 msgid "One-to-one relationship" msgstr "Relacija jedan na jedan" -#: db/models/fields/related.py:980 +#: db/models/fields/related.py:981 msgid "Many-to-many relationship" msgstr "Relacija više na više" -#: db/models/fields/related.py:1000 -msgid "" -"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "" -"Držite „Control“, ili „Command“ na Mac-u da biste obeležili više od jedne " -"stavke." +#: db/models/fields/related.py:1001 +msgid "Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "Držite „Control“, ili „Command“ na Mac-u da biste obeležili više od jedne stavke." -#: db/models/fields/related.py:1061 +#: db/models/fields/related.py:1062 #, python-format msgid "Please enter valid %(self)s IDs. The value %(value)r is invalid." -msgid_plural "" -"Please enter valid %(self)s IDs. The values %(value)r are invalid." +msgid_plural "Please enter valid %(self)s IDs. The values %(value)r are invalid." msgstr[0] "Unesite ispravan %(self)s IDs. Vrednost %(value)r je neispravna." msgstr[1] "Unesite ispravan %(self)s IDs. Vrednosti %(value)r su neispravne." msgstr[2] "Unesite ispravan %(self)s IDs. Vrednosti %(value)r su neispravne." @@ -4695,80 +4591,79 @@ msgstr[2] "Unesite ispravan %(self)s IDs. Vrednosti %(value)r su neispravne." msgid "This field is required." msgstr "Ovo polje se mora popuniti." -#: forms/fields.py:204 +#: forms/fields.py:203 msgid "Enter a whole number." msgstr "Unesite ceo broj." -#: forms/fields.py:235 forms/fields.py:256 +#: forms/fields.py:234 +#: forms/fields.py:255 msgid "Enter a number." msgstr "Unesite broj." -#: forms/fields.py:259 +#: forms/fields.py:258 #, python-format msgid "Ensure that there are no more than %s digits in total." msgstr "Ne sme biti ukupno više od %s cifara. Proverite." -#: forms/fields.py:260 +#: forms/fields.py:259 #, python-format msgid "Ensure that there are no more than %s decimal places." msgstr "Ne sme biti ukupno više od %s decimalnih mesta. Proverite." -#: forms/fields.py:261 +#: forms/fields.py:260 #, python-format msgid "Ensure that there are no more than %s digits before the decimal point." msgstr "Ne sme biti ukupno više od %s cifara pre zapete. Proverite." -#: forms/fields.py:323 forms/fields.py:838 +#: forms/fields.py:322 +#: forms/fields.py:837 msgid "Enter a valid date." msgstr "Unesite ispravan datum." -#: forms/fields.py:351 forms/fields.py:839 +#: forms/fields.py:350 +#: forms/fields.py:838 msgid "Enter a valid time." msgstr "Unesite ispravno vreme" -#: forms/fields.py:377 +#: forms/fields.py:376 msgid "Enter a valid date/time." msgstr "Unesite ispravan datum/vreme." -#: forms/fields.py:435 +#: forms/fields.py:434 msgid "No file was submitted. Check the encoding type on the form." msgstr "Fajl nije prebačen. Proverite tip enkodiranja formulara." -#: forms/fields.py:436 +#: forms/fields.py:435 msgid "No file was submitted." msgstr "Fajl nije prebačen." -#: forms/fields.py:437 +#: forms/fields.py:436 msgid "The submitted file is empty." msgstr "Prebačen fajl je prazan." -#: forms/fields.py:438 +#: forms/fields.py:437 #, python-format -msgid "" -"Ensure this filename has at most %(max)d characters (it has %(length)d)." -msgstr "" -"Naziv fajla mora da sadrži bar %(max)d slovnih mesta (trenutno ima %(length)" -"d)." +msgid "Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr "Naziv fajla mora da sadrži bar %(max)d slovnih mesta (trenutno ima %(length)d)." -#: forms/fields.py:473 -msgid "" -"Upload a valid image. The file you uploaded was either not an image or a " -"corrupted image." -msgstr "" -"Prebacite ispravan fajl. Fajl koji je prebačen ili nije slika, ili je " -"oštećen." +#: forms/fields.py:472 +msgid "Upload a valid image. The file you uploaded was either not an image or a corrupted image." +msgstr "Prebacite ispravan fajl. Fajl koji je prebačen ili nije slika, ili je oštećen." -#: forms/fields.py:596 forms/fields.py:671 +#: forms/fields.py:595 +#: forms/fields.py:670 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." -msgstr "" -"%(value)s nije među ponuđenim vrednostima. Odaberite jednu od ponuđenih." +msgstr "%(value)s nije među ponuđenim vrednostima. Odaberite jednu od ponuđenih." -#: forms/fields.py:672 forms/fields.py:734 forms/models.py:1002 +#: forms/fields.py:671 +#: forms/fields.py:733 +#: forms/models.py:1002 msgid "Enter a list of values." msgstr "Unesite listu vrednosti." -#: forms/formsets.py:298 forms/formsets.py:300 +#: forms/formsets.py:296 +#: forms/formsets.py:298 msgid "Order" msgstr "Redosled" @@ -4780,17 +4675,12 @@ msgstr "Ispravite dupliran sadržaj za polja: %(field)s." #: forms/models.py:566 #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." -msgstr "" -"Ispravite dupliran sadržaj za polja: %(field)s, koji mora da bude jedinstven." +msgstr "Ispravite dupliran sadržaj za polja: %(field)s, koji mora da bude jedinstven." #: forms/models.py:572 #, 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 "" -"Ispravite dupliran sadržaj za polja: %(field_name)s, koji mora da bude " -"jedinstven za %(lookup)s u %(date_field)s." +msgid "Please correct the duplicate data for %(field_name)s which must be unique for the %(lookup)s in %(date_field)s." +msgstr "Ispravite dupliran sadržaj za polja: %(field_name)s, koji mora da bude jedinstven za %(lookup)s u %(date_field)s." #: forms/models.py:580 msgid "Please correct the duplicate values below." @@ -4828,18 +4718,18 @@ msgstr[2] "%(size)d bajtova" #: template/defaultfilters.py:809 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:812 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." @@ -4929,23 +4819,28 @@ msgstr "januar" msgid "February" msgstr "februar" -#: utils/dates.py:18 utils/dates.py:31 +#: utils/dates.py:18 +#: utils/dates.py:31 msgid "March" msgstr "mart" -#: utils/dates.py:18 utils/dates.py:31 +#: utils/dates.py:18 +#: utils/dates.py:31 msgid "April" msgstr "april" -#: utils/dates.py:18 utils/dates.py:31 +#: utils/dates.py:18 +#: utils/dates.py:31 msgid "May" msgstr "maj" -#: utils/dates.py:18 utils/dates.py:31 +#: utils/dates.py:18 +#: utils/dates.py:31 msgid "June" msgstr "jun" -#: utils/dates.py:19 utils/dates.py:31 +#: utils/dates.py:19 +#: utils/dates.py:31 msgid "July" msgstr "jul" @@ -5105,23 +5000,23 @@ msgstr "%(number)d %(type)s" msgid ", %(number)d %(type)s" msgstr ", %(number)d %(type)s" -#: utils/translation/trans_real.py:518 +#: utils/translation/trans_real.py:519 msgid "DATE_FORMAT" msgstr "j. F Y." -#: utils/translation/trans_real.py:519 +#: utils/translation/trans_real.py:520 msgid "DATETIME_FORMAT" msgstr "j. F Y. H:i T" -#: utils/translation/trans_real.py:520 +#: utils/translation/trans_real.py:521 msgid "TIME_FORMAT" msgstr "G:i" -#: utils/translation/trans_real.py:541 +#: utils/translation/trans_real.py:542 msgid "YEAR_MONTH_FORMAT" msgstr "F Y." -#: utils/translation/trans_real.py:542 +#: utils/translation/trans_real.py:543 msgid "MONTH_DAY_FORMAT" msgstr "j. F" @@ -5139,3 +5034,4 @@ msgstr "%(verbose_name)s je uspešno ažuriran." #, python-format msgid "The %(verbose_name)s was deleted." msgstr "%(verbose_name)s je obrisan." + diff --git a/django/conf/locale/sr_Latn/formats.py b/django/conf/locale/sr_Latn/formats.py index 63a20f4574a2..cb0478ed0f59 100644 --- a/django/conf/locale/sr_Latn/formats.py +++ b/django/conf/locale/sr_Latn/formats.py @@ -11,9 +11,9 @@ SHORT_DATETIME_FORMAT = 'j.m.Y. H:i' FIRST_DAY_OF_WEEK = 1 DATE_INPUT_FORMATS = ( - '%Y-%m-%d', # '2006-10-25' '%d.%m.%Y.', '%d.%m.%y.', # '25.10.2006.', '25.10.06.' '%d. %m. %Y.', '%d. %m. %y.', # '25. 10. 2006.', '25. 10. 06.' + '%Y-%m-%d', # '2006-10-25' # '%d. %b %y.', '%d. %B %y.', # '25. Oct 06.', '25. October 06.' # '%d. %b \'%y.', '%d. %B \'%y.', # '25. Oct '06.', '25. October '06.' # '%d. %b %Y.', '%d. %B %Y.', # '25. Oct 2006.', '25. October 2006.' @@ -23,9 +23,6 @@ '%H:%M', # '14:30' ) DATETIME_INPUT_FORMATS = ( - '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' - '%Y-%m-%d %H:%M', # '2006-10-25 14:30' - '%Y-%m-%d', # '2006-10-25' '%d.%m.%Y. %H:%M:%S', # '25.10.2006. 14:30:59' '%d.%m.%Y. %H:%M', # '25.10.2006. 14:30' '%d.%m.%Y.', # '25.10.2006.' @@ -38,7 +35,10 @@ '%d. %m. %y. %H:%M:%S', # '25. 10. 06. 14:30:59' '%d. %m. %y. %H:%M', # '25. 10. 06. 14:30' '%d. %m. %y.', # '25. 10. 06.' + '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' + '%Y-%m-%d %H:%M', # '2006-10-25 14:30' + '%Y-%m-%d', # '2006-10-25' ) -DECIMAL_SEPARATOR = '.' -THOUSAND_SEPARATOR = ',' +DECIMAL_SEPARATOR = ',' +THOUSAND_SEPARATOR = '.' NUMBER_GROUPING = 3 diff --git a/django/conf/locale/sv/LC_MESSAGES/django.mo b/django/conf/locale/sv/LC_MESSAGES/django.mo index f65754a3c1e4..194d38b37189 100644 Binary files a/django/conf/locale/sv/LC_MESSAGES/django.mo and b/django/conf/locale/sv/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/sv/LC_MESSAGES/django.po b/django/conf/locale/sv/LC_MESSAGES/django.po index 749d2c79dc94..ff4372d813b0 100644 --- a/django/conf/locale/sv/LC_MESSAGES/django.po +++ b/django/conf/locale/sv/LC_MESSAGES/django.po @@ -4092,18 +4092,18 @@ msgstr[1] "%(size)d byte" #: template/defaultfilters.py:800 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:802 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:803 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/te/LC_MESSAGES/django.mo b/django/conf/locale/te/LC_MESSAGES/django.mo index 4c481def27b5..03048f88eff8 100644 Binary files a/django/conf/locale/te/LC_MESSAGES/django.mo and b/django/conf/locale/te/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/te/LC_MESSAGES/django.po b/django/conf/locale/te/LC_MESSAGES/django.po index 1f5abe174bd3..ae8a231d0f0c 100644 --- a/django/conf/locale/te/LC_MESSAGES/django.po +++ b/django/conf/locale/te/LC_MESSAGES/django.po @@ -3632,18 +3632,18 @@ msgstr[1] "%(size)d bytes" #: template/defaultfilters.py:739 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:741 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:742 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/th/LC_MESSAGES/django.mo b/django/conf/locale/th/LC_MESSAGES/django.mo index 026ba1b1f8e9..6561e7f0fe2a 100644 Binary files a/django/conf/locale/th/LC_MESSAGES/django.mo and b/django/conf/locale/th/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/th/LC_MESSAGES/django.po b/django/conf/locale/th/LC_MESSAGES/django.po index fbf67de078da..75a4d3d1cfcc 100644 --- a/django/conf/locale/th/LC_MESSAGES/django.po +++ b/django/conf/locale/th/LC_MESSAGES/django.po @@ -3631,18 +3631,18 @@ msgstr[0] "%(size)d ไบต์" #: template/defaultfilters.py:739 #, python-format -msgid "%.1f KB" -msgstr "%.1f กิโลไบต์" +msgid "%s KB" +msgstr "%s กิโลไบต์" #: template/defaultfilters.py:741 #, python-format -msgid "%.1f MB" -msgstr "%.1f เมกะไบต์" +msgid "%s MB" +msgstr "%s เมกะไบต์" #: template/defaultfilters.py:742 #, python-format -msgid "%.1f GB" -msgstr "%.1f กิกะไบต์" +msgid "%s GB" +msgstr "%s กิกะไบต์" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/tr/LC_MESSAGES/django.mo b/django/conf/locale/tr/LC_MESSAGES/django.mo index a7926a1b2fc5..1fc73516b76d 100644 Binary files a/django/conf/locale/tr/LC_MESSAGES/django.mo and b/django/conf/locale/tr/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/tr/LC_MESSAGES/django.po b/django/conf/locale/tr/LC_MESSAGES/django.po index bf4f766cf66e..22110b888b70 100644 --- a/django/conf/locale/tr/LC_MESSAGES/django.po +++ b/django/conf/locale/tr/LC_MESSAGES/django.po @@ -4812,18 +4812,18 @@ msgstr[0] "%(size)d bayt" #: template/defaultfilters.py:814 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:816 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:817 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/uk/LC_MESSAGES/django.mo b/django/conf/locale/uk/LC_MESSAGES/django.mo index f57a32b1c832..2bd542ff6de6 100644 Binary files a/django/conf/locale/uk/LC_MESSAGES/django.mo and b/django/conf/locale/uk/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/uk/LC_MESSAGES/django.po b/django/conf/locale/uk/LC_MESSAGES/django.po index 8589a6cf5b7d..f7793d820aa8 100644 --- a/django/conf/locale/uk/LC_MESSAGES/django.po +++ b/django/conf/locale/uk/LC_MESSAGES/django.po @@ -4330,18 +4330,18 @@ msgstr[1] "%(size)d байтів" #: .\template\defaultfilters.py:776 #, python-format -msgid "%.1f KB" -msgstr "%.1f КБ" +msgid "%s KB" +msgstr "%s КБ" #: .\template\defaultfilters.py:778 #, python-format -msgid "%.1f MB" -msgstr "%.1f МБ" +msgid "%s MB" +msgstr "%s МБ" #: .\template\defaultfilters.py:779 #, python-format -msgid "%.1f GB" -msgstr "%.1f ГБ" +msgid "%s GB" +msgstr "%s ГБ" #: .\utils\dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/uk/formats.py b/django/conf/locale/uk/formats.py index 8e41bf020d57..8c01f3ddbded 100644 --- a/django/conf/locale/uk/formats.py +++ b/django/conf/locale/uk/formats.py @@ -14,5 +14,5 @@ # TIME_INPUT_FORMATS = # DATETIME_INPUT_FORMATS = DECIMAL_SEPARATOR = ',' -THOUSAND_SEPARATOR = ' ' +THOUSAND_SEPARATOR = u' ' # NUMBER_GROUPING = diff --git a/django/conf/locale/vi/LC_MESSAGES/django.mo b/django/conf/locale/vi/LC_MESSAGES/django.mo index be577cc7b569..f0446261d56f 100644 Binary files a/django/conf/locale/vi/LC_MESSAGES/django.mo and b/django/conf/locale/vi/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/vi/LC_MESSAGES/django.po b/django/conf/locale/vi/LC_MESSAGES/django.po index b3d16bc96a60..8d03f350ad63 100644 --- a/django/conf/locale/vi/LC_MESSAGES/django.po +++ b/django/conf/locale/vi/LC_MESSAGES/django.po @@ -4724,18 +4724,18 @@ msgstr[1] "" #: template/defaultfilters.py:808 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:810 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:811 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:42 msgid "p.m." diff --git a/django/conf/locale/zh_CN/LC_MESSAGES/django.mo b/django/conf/locale/zh_CN/LC_MESSAGES/django.mo index 2c5835631120..6d2f546d942d 100644 Binary files a/django/conf/locale/zh_CN/LC_MESSAGES/django.mo and b/django/conf/locale/zh_CN/LC_MESSAGES/django.mo differ diff --git a/django/conf/locale/zh_CN/LC_MESSAGES/django.po b/django/conf/locale/zh_CN/LC_MESSAGES/django.po index 8bba862c5df7..24643e6e9c65 100644 --- a/django/conf/locale/zh_CN/LC_MESSAGES/django.po +++ b/django/conf/locale/zh_CN/LC_MESSAGES/django.po @@ -3911,18 +3911,18 @@ msgstr[0] "%(size)d 字节" #: template/defaultfilters.py:776 #, python-format -msgid "%.1f KB" -msgstr "%.1f KB" +msgid "%s KB" +msgstr "%s KB" #: template/defaultfilters.py:778 #, python-format -msgid "%.1f MB" -msgstr "%.1f MB" +msgid "%s MB" +msgstr "%s MB" #: template/defaultfilters.py:779 #, python-format -msgid "%.1f GB" -msgstr "%.1f GB" +msgid "%s GB" +msgstr "%s GB" #: utils/dateformat.py:41 msgid "p.m." diff --git a/django/conf/locale/zh_TW/LC_MESSAGES/django.po b/django/conf/locale/zh_TW/LC_MESSAGES/django.po index 0efe779eb9f1..7ad2c09796c5 100644 --- a/django/conf/locale/zh_TW/LC_MESSAGES/django.po +++ b/django/conf/locale/zh_TW/LC_MESSAGES/django.po @@ -3836,17 +3836,17 @@ msgstr[0] "" #: template/defaultfilters.py:739 #, python-format -msgid "%.1f KB" +msgid "%s KB" msgstr "" #: template/defaultfilters.py:741 #, python-format -msgid "%.1f MB" +msgid "%s MB" msgstr "" #: template/defaultfilters.py:742 #, python-format -msgid "%.1f GB" +msgid "%s GB" msgstr "" #: utils/dateformat.py:41 diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index ee4c9b5e9654..686fadcc4588 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -43,7 +43,7 @@ # calendars according to the current locale USE_L10N = True -# Absolute path to the directory that holds media. +# Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = '' @@ -91,4 +91,6 @@ 'django.contrib.messages', # Uncomment the next line to enable the admin: # 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', ) diff --git a/django/conf/project_template/urls.py b/django/conf/project_template/urls.py index dfb49d3bdcac..3d0ff636a50a 100644 --- a/django/conf/project_template/urls.py +++ b/django/conf/project_template/urls.py @@ -8,8 +8,7 @@ # Example: # (r'^{{ project_name }}/', include('{{ project_name }}.foo.urls')), - # Uncomment the admin/doc line below and add 'django.contrib.admindocs' - # to INSTALLED_APPS to enable admin documentation: + # Uncomment the admin/doc line below to enable admin documentation: # (r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index 6bcaf80248af..83af5a180384 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -1,7 +1,7 @@ from django import forms from django.conf import settings from django.contrib.admin.util import flatten_fieldsets, lookup_field -from django.contrib.admin.util import display_for_field, label_for_field +from django.contrib.admin.util import display_for_field, label_for_field, help_text_for_field from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ObjectDoesNotExist from django.db.models.fields import FieldDoesNotExist @@ -128,6 +128,9 @@ def label_tag(self): attrs = classes and {'class': u' '.join(classes)} or {} return self.field.label_tag(contents=contents, attrs=attrs) + def errors(self): + return mark_safe(self.field.errors.as_ul()) + class AdminReadonlyField(object): def __init__(self, form, field, is_first, model_admin=None): label = label_for_field(field, form._meta.model, model_admin) @@ -142,6 +145,7 @@ def __init__(self, form, field, is_first, model_admin=None): 'name': class_name, 'label': label, 'field': field, + 'help_text': help_text_for_field(class_name, form._meta.model) } self.form = form self.model_admin = model_admin @@ -203,14 +207,14 @@ def __iter__(self): for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original, self.readonly_fields, - model_admin=self.model_admin) + model_admin=self.opts) for form in self.formset.extra_forms: yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None, self.readonly_fields, - model_admin=self.model_admin) + model_admin=self.opts) yield InlineAdminForm(self.formset, self.formset.empty_form, self.fieldsets, self.opts.prepopulated_fields, None, - self.readonly_fields, model_admin=self.model_admin) + self.readonly_fields, model_admin=self.opts) def fields(self): fk = getattr(self.formset, "fk", None) @@ -219,7 +223,7 @@ def fields(self): continue if field in self.readonly_fields: yield { - 'label': label_for_field(field, self.opts.model, self.model_admin), + 'label': label_for_field(field, self.opts.model, self.opts), 'widget': { 'is_hidden': False }, diff --git a/django/contrib/admin/media/css/base.css b/django/contrib/admin/media/css/base.css index da502f357a46..9a7b656ffe1e 100644 --- a/django/contrib/admin/media/css/base.css +++ b/django/contrib/admin/media/css/base.css @@ -319,11 +319,11 @@ table thead th.sorted a { } table thead th.ascending a { - background: url(../img/admin/arrow-down.gif) right .4em no-repeat; + background: url(../img/admin/arrow-up.gif) right .4em no-repeat; } table thead th.descending a { - background: url(../img/admin/arrow-up.gif) right .4em no-repeat; + background: url(../img/admin/arrow-down.gif) right .4em no-repeat; } /* ORDERABLE TABLES */ @@ -445,6 +445,14 @@ ul.messagelist li { background: #ffc url(../img/admin/icon_success.gif) 5px .3em no-repeat; } +ul.messagelist li.warning{ + background-image: url(../img/admin/icon_alert.gif); +} + +ul.messagelist li.error{ + background-image: url(../img/admin/icon_error.gif); +} + .errornote { font-size: 12px !important; display: block; @@ -470,6 +478,11 @@ ul.errorlist { background: red url(../img/admin/icon_alert.gif) 5px .3em no-repeat; } +.errorlist li a { + color: white; + text-decoration: underline; +} + td ul.errorlist { margin: 0 !important; padding: 0 !important; @@ -483,7 +496,7 @@ td ul.errorlist li { background: #ffc; } -.errors input, .errors select { +.errors input, .errors select, .errors textarea { border: 1px solid red; } diff --git a/django/contrib/admin/media/css/changelists.css b/django/contrib/admin/media/css/changelists.css index 99ff8bc0cf42..f6acdeb8956b 100644 --- a/django/contrib/admin/media/css/changelists.css +++ b/django/contrib/admin/media/css/changelists.css @@ -9,6 +9,8 @@ width: 100%; } +.change-list .hiddenfields { display:none; } + .change-list .filtered table { border-right: 1px solid #ddd; } @@ -58,14 +60,17 @@ text-align: center; } -#changelist table tbody td { +#changelist table tbody td, #changelist table tbody th { border-left: 1px solid #ddd; } -#changelist table tbody td:first-child { +#changelist table tbody td:first-child, #changelist table tbody th:first-child { border-left: 0; border-right: 1px solid #ddd; - text-align: center; +} + +#changelist table tbody td.action-checkbox { + text-align:center; } #changelist table tfoot { diff --git a/django/contrib/admin/media/css/rtl.css b/django/contrib/admin/media/css/rtl.css index acf640e02076..b05537a26e92 100644 --- a/django/contrib/admin/media/css/rtl.css +++ b/django/contrib/admin/media/css/rtl.css @@ -82,6 +82,10 @@ div.breadcrumbs { /* changelists styles */ +.change-list ul.toplinks li { + float: right; +} + .change-list .filtered { background: white url(../img/admin/changelist-bg_rtl.gif) top left repeat-y !important; } diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css index 620e08289ada..c0911ff97294 100644 --- a/django/contrib/admin/media/css/widgets.css +++ b/django/contrib/admin/media/css/widgets.css @@ -70,6 +70,7 @@ height: 16px; display: block; text-indent: -3000px; + overflow: hidden; } .selector-add { diff --git a/django/contrib/admin/media/js/admin/DateTimeShortcuts.js b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js index 2e43a68674d6..a4293b305bb4 100644 --- a/django/contrib/admin/media/js/admin/DateTimeShortcuts.js +++ b/django/contrib/admin/media/js/admin/DateTimeShortcuts.js @@ -94,7 +94,7 @@ var DateTimeShortcuts = { openClock: function(num) { var clock_box = document.getElementById(DateTimeShortcuts.clockDivName+num) var clock_link = document.getElementById(DateTimeShortcuts.clockLinkName+num) - + // Recalculate the clockbox position // is it left-to-right or right-to-left layout ? if (getStyle(document.body,'direction')!='rtl') { @@ -107,8 +107,8 @@ var DateTimeShortcuts = { // (it returns as it was left aligned), needs to be fixed. clock_box.style.left = findPosX(clock_link) - 110 + 'px'; } - clock_box.style.top = findPosY(clock_link) - 30 + 'px'; - + clock_box.style.top = Math.max(0, findPosY(clock_link) - 30) + 'px'; + // Show the clock box clock_box.style.display = 'block'; addEvent(window.document, 'click', function() { DateTimeShortcuts.dismissClock(num); return true; }); @@ -224,8 +224,8 @@ var DateTimeShortcuts = { // (it returns as it was left aligned), needs to be fixed. cal_box.style.left = findPosX(cal_link) - 180 + 'px'; } - cal_box.style.top = findPosY(cal_link) - 75 + 'px'; - + cal_box.style.top = Math.max(0, findPosY(cal_link) - 75) + 'px'; + cal_box.style.display = 'block'; addEvent(window.document, 'click', function() { DateTimeShortcuts.dismissCalendar(num); return true; }); }, diff --git a/django/contrib/admin/media/js/inlines.js b/django/contrib/admin/media/js/inlines.js index cf790235c089..bddd6f75ba3d 100644 --- a/django/contrib/admin/media/js/inlines.js +++ b/django/contrib/admin/media/js/inlines.js @@ -18,7 +18,7 @@ $.fn.formset = function(opts) { var options = $.extend({}, $.fn.formset.defaults, opts); var updateElementIndex = function(el, prefix, ndx) { - var id_regex = new RegExp("(" + prefix + "-\\d+)"); + var id_regex = new RegExp("(" + prefix + "-(\\d+|__prefix__))"); var replacement = prefix + "-" + ndx; if ($(el).attr("for")) { $(el).attr("for", $(el).attr("for").replace(id_regex, replacement)); @@ -31,6 +31,7 @@ } }; var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS").attr("autocomplete", "off"); + var nextIndex = parseInt(totalForms.val()); var maxForms = $("#id_" + options.prefix + "-MAX_NUM_FORMS").attr("autocomplete", "off"); // only show the add button if we are allowed to add more items, // note that max_num = None translates to a blank string. @@ -53,29 +54,11 @@ } addButton.click(function() { var totalForms = $("#id_" + options.prefix + "-TOTAL_FORMS"); - var nextIndex = parseInt(totalForms.val()); var template = $("#" + options.prefix + "-empty"); var row = template.clone(true); row.removeClass(options.emptyCssClass) .addClass(options.formCssClass) - .attr("id", options.prefix + "-" + nextIndex) - .insertBefore($(template)); - row.find("*") - .filter(function() { - var el = $(this); - return el.attr("id") && el.attr("id").search(/__prefix__/) >= 0; - }).each(function() { - var el = $(this); - el.attr("id", el.attr("id").replace(/__prefix__/g, nextIndex)); - }) - .end() - .filter(function() { - var el = $(this); - return el.attr("name") && el.attr("name").search(/__prefix__/) >= 0; - }).each(function() { - var el = $(this); - el.attr("name", el.attr("name").replace(/__prefix__/g, nextIndex)); - }); + .attr("id", options.prefix + "-" + nextIndex); if (row.is("tr")) { // If the forms are laid out in table rows, insert // the remove button into the last table cell: @@ -89,11 +72,14 @@ // last child element of the form's container: row.children(":first").append('' + options.deleteText + ""); } - row.find("input,select,textarea,label,a").each(function() { + row.find("*").each(function() { updateElementIndex(this, options.prefix, totalForms.val()); }); + // Insert the new form when it has been fully edited + row.insertBefore($(template)); // Update number of total forms - $(totalForms).val(nextIndex + 1); + $(totalForms).val(parseInt(totalForms.val()) + 1); + nextIndex += 1; // Hide add button in case we've hit the max, except we want to add infinitely if ((maxForms.val() != '') && (maxForms.val()-totalForms.val()) <= 0) { addButton.parent().hide(); @@ -103,6 +89,7 @@ // Remove the parent form containing this button: var row = $(this).parents("." + options.formCssClass); row.remove(); + nextIndex -= 1; // If a post-delete callback was provided, call it with the deleted form: if (options.removed) { options.removed(row); @@ -118,7 +105,8 @@ // so they remain in sequence: for (var i=0, formCount=forms.length; i0;a(this).each(function(){a(this).not("."+b.emptyCssClass).addClass(b.formCssClass)}); -if(a(this).length&&g){var i;if(a(this).attr("tagName")=="TR"){g=this.eq(0).children().length;a(this).parent().append(''+b.addText+"");i=a(this).parent().find("tr:last a")}else{a(this).filter(":last").after('");i=a(this).filter(":last").next().find("a")}i.click(function(){var e=a("#id_"+b.prefix+"-TOTAL_FORMS"),f=parseInt(e.val()), -j=a("#"+b.prefix+"-empty"),d=j.clone(true);d.removeClass(b.emptyCssClass).addClass(b.formCssClass).attr("id",b.prefix+"-"+f).insertBefore(a(j));d.find("*").filter(function(){var c=a(this);return c.attr("id")&&c.attr("id").search(/__prefix__/)>=0}).each(function(){var c=a(this);c.attr("id",c.attr("id").replace(/__prefix__/g,f))}).end().filter(function(){var c=a(this);return c.attr("name")&&c.attr("name").search(/__prefix__/)>=0}).each(function(){var c=a(this);c.attr("name",c.attr("name").replace(/__prefix__/g, -f))});if(d.is("tr"))d.children(":last").append('");else d.is("ul")||d.is("ol")?d.append('
      • '+b.deleteText+"
      • "):d.children(":first").append(''+b.deleteText+"");d.find("input,select,textarea,label,a").each(function(){l(this,b.prefix,e.val())});a(e).val(f+1);h.val()!=""&& -h.val()-e.val()<=0&&i.parent().hide();d.find("a."+b.deleteCssClass).click(function(){var c=a(this).parents("."+b.formCssClass);c.remove();b.removed&&b.removed(c);c=a("."+b.formCssClass);a("#id_"+b.prefix+"-TOTAL_FORMS").val(c.length);if(h.val()==""||h.val()-c.length>0)i.parent().show();for(var k=0,m=c.length;k0;b(this).each(function(){b(this).not("."+ +a.emptyCssClass).addClass(a.formCssClass)});if(b(this).length&&g){var j;if(b(this).attr("tagName")=="TR"){g=this.eq(0).children().length;b(this).parent().append(''+a.addText+"");j=b(this).parent().find("tr:last a")}else{b(this).filter(":last").after('");j=b(this).filter(":last").next().find("a")}j.click(function(){var c=b("#id_"+ +a.prefix+"-TOTAL_FORMS"),f=b("#"+a.prefix+"-empty"),d=f.clone(true);d.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+l);if(d.is("tr"))d.children(":last").append('");else d.is("ul")||d.is("ol")?d.append('
      • '+a.deleteText+"
      • "):d.children(":first").append(''+ +a.deleteText+"");d.find("*").each(function(){k(this,a.prefix,c.val())});d.insertBefore(b(f));b(c).val(parseInt(c.val())+1);l+=1;h.val()!=""&&h.val()-c.val()<=0&&j.parent().hide();d.find("a."+a.deleteCssClass).click(function(){var e=b(this).parents("."+a.formCssClass);e.remove();l-=1;a.removed&&a.removed(e);e=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(e.length);if(h.val()==""||h.val()-e.length>0)j.parent().show();for(var i=0,m=e.length;i 0) { - values.push($(this).val()); - } - }); + $.each(dependencies, function(i, field) { + if ($(field).val().length > 0) { + values.push($(field).val()); + } + }) field.val(URLify(values.join(' '), maxLength)); }; - dependencies.keyup(populate).change(populate).focus(populate); + $(dependencies.join(',')).keyup(populate).change(populate).focus(populate); }); }; })(django.jQuery); diff --git a/django/contrib/admin/media/js/prepopulate.min.js b/django/contrib/admin/media/js/prepopulate.min.js index f1ca297b8050..aa94937963eb 100644 --- a/django/contrib/admin/media/js/prepopulate.min.js +++ b/django/contrib/admin/media/js/prepopulate.min.js @@ -1 +1 @@ -(function(b){b.fn.prepopulate=function(d,f){return this.each(function(){var a=b(this);a.data("_changed",false);a.change(function(){a.data("_changed",true)});var c=function(){if(a.data("_changed")!=true){var e=[];d.each(function(){b(this).val().length>0&&e.push(b(this).val())});a.val(URLify(e.join(" "),f))}};d.keyup(c).change(c).focus(c)})}})(django.jQuery); +(function(a){a.fn.prepopulate=function(d,g){return this.each(function(){var b=a(this);b.data("_changed",false);b.change(function(){b.data("_changed",true)});var c=function(){if(b.data("_changed")!=true){var e=[];a.each(d,function(h,f){a(f).val().length>0&&e.push(a(f).val())});b.val(URLify(e.join(" "),g))}};a(d.join(",")).keyup(c).change(c).focus(c)})}})(django.jQuery); diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 1f8ff6dbd189..b151cf2626a6 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -10,13 +10,15 @@ from django.views.decorators.csrf import csrf_protect from django.core.exceptions import PermissionDenied, ValidationError from django.db import models, transaction -from django.db.models.fields import BLANK_CHOICE_DASH +from django.db.models.related import RelatedObject +from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist +from django.db.models.sql.constants import LOOKUP_SEP, QUERY_TERMS from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response from django.utils.decorators import method_decorator from django.utils.datastructures import SortedDict from django.utils.functional import update_wrapper -from django.utils.html import escape +from django.utils.html import escape, escapejs from django.utils.safestring import mark_safe from django.utils.functional import curry from django.utils.text import capfirst, get_text_list @@ -183,6 +185,53 @@ def _declared_fieldsets(self): def get_readonly_fields(self, request, obj=None): return self.readonly_fields + def lookup_allowed(self, lookup, value): + model = self.model + # Check FKey lookups that are allowed, so that popups produced by + # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to, + # are allowed to work. + for l in model._meta.related_fkey_lookups: + for k, v in widgets.url_params_from_lookup_dict(l).items(): + if k == lookup and v == value: + return True + + parts = lookup.split(LOOKUP_SEP) + + # Last term in lookup is a query term (__exact, __startswith etc) + # This term can be ignored. + if len(parts) > 1 and parts[-1] in QUERY_TERMS: + parts.pop() + + # Special case -- foo__id__exact and foo__id queries are implied + # if foo has been specificially included in the lookup list; so + # drop __id if it is the last part. However, first we need to find + # the pk attribute name. + pk_attr_name = None + for part in parts[:-1]: + field, _, _, _ = model._meta.get_field_by_name(part) + if hasattr(field, 'rel'): + model = field.rel.to + pk_attr_name = model._meta.pk.name + elif isinstance(field, RelatedObject): + model = field.model + pk_attr_name = model._meta.pk.name + else: + pk_attr_name = None + if pk_attr_name and len(parts) > 1 and parts[-1] == pk_attr_name: + parts.pop() + + try: + self.model._meta.get_field_by_name(parts[0]) + except FieldDoesNotExist: + # Lookups on non-existants fields are ok, since they're ignored + # later. + return True + else: + if len(parts) == 1: + return True + clean_lookup = LOOKUP_SEP.join(parts) + return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy + class ModelAdmin(BaseModelAdmin): "Encapsulates all admin options and functionality for a given model." @@ -281,17 +330,23 @@ def _media(self): media = property(_media) def has_add_permission(self, request): - "Returns True if the given request has permission to add an object." + """ + Returns True if the given request has permission to add an object. + Can be overriden by the user in subclasses. + """ opts = self.opts return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) def has_change_permission(self, request, obj=None): """ Returns True if the given request has permission to change the given - Django model instance. + Django model instance, the default implementation doesn't examine the + `obj` parameter. - If `obj` is None, this should return True if the given request has - permission to change *any* object of the given type. + Can be overriden by the user in subclasses. In such case it should + return True if the given request has permission to change the `obj` + model instance. If `obj` is None, this should return True if the given + request has permission to change *any* object of the given type. """ opts = self.opts return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) @@ -299,10 +354,13 @@ def has_change_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None): """ Returns True if the given request has permission to change the given - Django model instance. + Django model instance, the default implementation doesn't examine the + `obj` parameter. - If `obj` is None, this should return True if the given request has - permission to delete *any* object of the given type. + Can be overriden by the user in subclasses. In such case it should + return True if the given request has permission to delete the `obj` + model instance. If `obj` is None, this should return True if the given + request has permission to delete *any* object of the given type. """ opts = self.opts return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) @@ -478,7 +536,7 @@ def get_actions(self, request): # If self.actions is explicitally set to None that means that we don't # want *any* actions enabled on this page. if self.actions is None: - return [] + return SortedDict() actions = [] @@ -652,7 +710,7 @@ def response_add(self, request, obj, post_url_continue='../%s/'): if request.POST.has_key("_popup"): return HttpResponse('' % \ # escape() calls force_unicode. - (escape(pk_value), escape(obj))) + (escape(pk_value), escapejs(obj))) elif request.POST.has_key("_addanother"): self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) return HttpResponseRedirect(request.path) @@ -673,9 +731,16 @@ def response_change(self, request, obj): Determines the HttpResponse for the change_view stage. """ opts = obj._meta + + # Handle proxy models automatically created by .only() or .defer() + verbose_name = opts.verbose_name + if obj._deferred: + opts_ = opts.proxy_for_model._meta + verbose_name = opts_.verbose_name + pk_value = obj._get_pk_val() - msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(opts.verbose_name), 'obj': force_unicode(obj)} + msg = _('The %(name)s "%(obj)s" was changed successfully.') % {'name': force_unicode(verbose_name), 'obj': force_unicode(obj)} if request.POST.has_key("_continue"): self.message_user(request, msg + ' ' + _("You may edit it again below.")) if request.REQUEST.has_key('_popup'): @@ -683,15 +748,21 @@ def response_change(self, request, obj): else: return HttpResponseRedirect(request.path) elif request.POST.has_key("_saveasnew"): - msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(opts.verbose_name), 'obj': obj} + msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % {'name': force_unicode(verbose_name), 'obj': obj} self.message_user(request, msg) return HttpResponseRedirect("../%s/" % pk_value) elif request.POST.has_key("_addanother"): - self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(opts.verbose_name))) + self.message_user(request, msg + ' ' + (_("You may add another %s below.") % force_unicode(verbose_name))) return HttpResponseRedirect("../add/") else: self.message_user(request, msg) - return HttpResponseRedirect("../") + # Figure out where to redirect. If the user has change permission, + # redirect to the change-list page for this object. Otherwise, + # redirect to the admin index. + if self.has_change_permission(request, None): + return HttpResponseRedirect('../') + else: + return HttpResponseRedirect('../../../') def response_action(self, request, queryset): """ @@ -754,7 +825,7 @@ def response_action(self, request, queryset): if isinstance(response, HttpResponse): return response else: - return HttpResponseRedirect(".") + return HttpResponseRedirect(request.get_full_path()) else: msg = _("No action selected.") self.message_user(request, msg) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 444649045105..8bee13350e33 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -3,6 +3,7 @@ from django.contrib.admin import ModelAdmin from django.contrib.admin import actions from django.contrib.auth import authenticate, login +from django.contrib.contenttypes import views as contenttype_views from django.views.decorators.csrf import csrf_protect from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured @@ -225,7 +226,7 @@ def wrapper(*args, **kwargs): wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), url(r'^r/(?P\d+)/(?P.+)/$', - 'django.views.defaults.shortcut'), + wrap(contenttype_views.shortcut)), url(r'^(?P\w+)/$', wrap(self.app_index), name='app_list') @@ -327,13 +328,11 @@ def login(self, request): try: user = User.objects.get(email=username) except (User.DoesNotExist, User.MultipleObjectsReturned): - message = _("Usernames cannot contain the '@' character.") + pass else: if user.check_password(password): message = _("Your e-mail address is not your username." " Try '%s' instead.") % user.username - else: - message = _("Usernames cannot contain the '@' character.") return self.display_login_form(request, message) # The user data is correct; log in the user in and continue. diff --git a/django/contrib/admin/templates/admin/auth/user/add_form.html b/django/contrib/admin/templates/admin/auth/user/add_form.html index b57f59b82eeb..c8889eb069d2 100644 --- a/django/contrib/admin/templates/admin/auth/user/add_form.html +++ b/django/contrib/admin/templates/admin/auth/user/add_form.html @@ -2,8 +2,11 @@ {% load i18n %} {% block form_top %} + {% if not is_popup %}

        {% trans "First, enter a username and password. Then, you'll be able to edit more user options." %}

        - + {% else %} +

        {% trans "Enter a username and password." %}

        + {% endif %} {% endblock %} {% block after_field_sets %} diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 3fd9eb3770b8..30a4e494face 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -1,5 +1,5 @@ - + {% block title %}{% endblock %} @@ -56,7 +56,9 @@ {% endif %} {% if messages %} -
          {% for message in messages %}
        • {{ message }}
        • {% endfor %}
        +
          {% for message in messages %} + {{ message }} + {% endfor %}
        {% endif %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 7c96628963c1..08347abe78cf 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -16,8 +16,8 @@ {% block breadcrumbs %}{% if not is_popup %} {% endif %}{% endblock %} @@ -56,7 +56,7 @@ {% submit_row %} {% if adminform and add %} - + {% endif %} {# JavaScript for prepopulated fields #} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 3b037e54f954..9ba9a0dbb58c 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -87,7 +87,7 @@

        {% trans 'Filter' %}

        {% csrf_token %} {% if cl.formset %} - {{ cl.formset.management_form }} +
        {{ cl.formset.management_form }}
        {% endif %} {% block result_list %} diff --git a/django/contrib/admin/templates/admin/change_list_results.html b/django/contrib/admin/templates/admin/change_list_results.html index 0efcc9b24a03..dcb266b03d52 100644 --- a/django/contrib/admin/templates/admin/change_list_results.html +++ b/django/contrib/admin/templates/admin/change_list_results.html @@ -1,3 +1,8 @@ +{% if result_hidden_fields %} +
        {# DIV for HTML validation #} +{% for item in result_hidden_fields %}{{ item }}{% endfor %} +
        +{% endif %} {% if results %} @@ -10,6 +15,9 @@ {% for result in results %} +{% if result.form.non_field_errors %} + +{% endif %} {% for item in result %}{{ item }}{% endfor %} {% endfor %} diff --git a/django/contrib/admin/templates/admin/edit_inline/stacked.html b/django/contrib/admin/templates/admin/edit_inline/stacked.html index 1ecb790b5e71..ff628c0bbf7a 100644 --- a/django/contrib/admin/templates/admin/edit_inline/stacked.html +++ b/django/contrib/admin/templates/admin/edit_inline/stacked.html @@ -41,11 +41,11 @@

        {{ inline_admin_formset.opts.verbose_name|title }}: {{ inline_admin_formset.opts.verbose_name|title }}: 

        {% for inline_admin_form in inline_admin_formset %} {% if inline_admin_form.form.non_field_errors %} - + {% endif %} @@ -87,11 +87,11 @@

        {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}

        $(".selectfilter").each(function(index, value){ var namearr = value.name.split('-'); SelectFilter.init(value.id, namearr[namearr.length-1], false, "{% admin_media_prefix %}"); - }) + }); $(".selectfilterstacked").each(function(index, value){ var namearr = value.name.split('-'); SelectFilter.init(value.id, namearr[namearr.length-1], true, "{% admin_media_prefix %}"); - }) + }); } } var initPrepopulatedFields = function(row) { @@ -99,7 +99,10 @@

        {{ inline_admin_formset.opts.verbose_name_plural|capfirst }}

        var field = $(this); var input = field.find('input, select, textarea'); var dependency_list = input.data('dependency_list') || []; - var dependencies = row.find(dependency_list.join(',')).find('input, select, textarea'); + var dependencies = []; + $.each(dependency_list, function(i, field_name) { + dependencies.push('#' + row.find(field_name).find('input, select, textarea').attr('id')); + }); if (dependencies.length) { input.prepopulate(dependencies, input.attr('maxlength')); } diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html index e3b1c6a38b30..6363aeeee4fa 100644 --- a/django/contrib/admin/templates/admin/includes/fieldset.html +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -4,10 +4,11 @@
        {{ fieldset.description|safe }}
        {% endif %} {% for line in fieldset %} -
        - {{ line.errors }} +
        + {% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %} {% for field in line %} - + + {% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %} {% if field.is_checkbox %} {{ field.field }}{{ field.label_tag }} {% else %} @@ -18,8 +19,8 @@ {{ field.field }} {% endif %} {% endif %} - {% if field.field.field.help_text %} -

        {{ field.field.field.help_text|safe }}

        + {% if field.field.help_text %} +

        {{ field.field.help_text|safe }}

        {% endif %}
        {% endfor %} diff --git a/django/contrib/admin/templates/admin/prepopulated_fields_js.html b/django/contrib/admin/templates/admin/prepopulated_fields_js.html index 4aa638031366..43ef5ba0717b 100644 --- a/django/contrib/admin/templates/admin/prepopulated_fields_js.html +++ b/django/contrib/admin/templates/admin/prepopulated_fields_js.html @@ -17,7 +17,7 @@ $('.empty-form .{{ field.field.name }}').addClass('prepopulated_field'); $(field.id).data('dependency_list', field['dependency_list']) - .prepopulate($(field['dependency_ids'].join(',')), field.maxLength); + .prepopulate(field['dependency_ids'], field.maxLength); {% endfor %} })(django.jQuery); diff --git a/django/contrib/admin/templates/admin/template_validator.html b/django/contrib/admin/templates/admin/template_validator.html deleted file mode 100644 index 9a139c5d4929..000000000000 --- a/django/contrib/admin/templates/admin/template_validator.html +++ /dev/null @@ -1,31 +0,0 @@ -{% extends "admin/base_site.html" %} - -{% block content %} - -
        - -{% csrf_token %} - -{% if form.errors %} -

        Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:

        -{% endif %} - -
        -
        - {{ form.errors.site }} -

        {{ form.site }}

        -
        -
        - {{ form.errors.template }} -

        {{ form.template }}

        -
        -
        - -
        - -
        - - -
        - -{% endblock %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 565db32251ce..27e0332e0b4e 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -140,6 +140,8 @@ def items_for_result(cl, result, form): result_repr = EMPTY_CHANGELIST_VALUE else: if f is None: + if field_name == u'action_checkbox': + row_class = ' class="action-checkbox"' allow_tags = getattr(attr, 'allow_tags', False) boolean = getattr(attr, 'boolean', False) if boolean: @@ -154,13 +156,17 @@ def items_for_result(cl, result, form): else: result_repr = mark_safe(result_repr) else: - if value is None: - result_repr = EMPTY_CHANGELIST_VALUE if isinstance(f.rel, models.ManyToOneRel): - result_repr = escape(getattr(result, f.name)) + field_val = getattr(result, f.name) + if field_val is None: + result_repr = EMPTY_CHANGELIST_VALUE + else: + result_repr = escape(field_val) else: result_repr = display_for_field(value, f) - if isinstance(f, models.DateField) or isinstance(f, models.TimeField): + if isinstance(f, models.DateField)\ + or isinstance(f, models.TimeField)\ + or isinstance(f, models.ForeignKey): row_class = ' class="nowrap"' if force_unicode(result_repr) == '': result_repr = mark_safe(' ') @@ -183,28 +189,46 @@ def items_for_result(cl, result, form): # By default the fields come from ModelAdmin.list_editable, but if we pull # the fields out of the form instead of list_editable custom admins # can provide fields on a per request basis - if form and field_name in form.fields: + if (form and field_name in form.fields and not ( + field_name == cl.model._meta.pk.name and + form[cl.model._meta.pk.name].is_hidden)): bf = form[field_name] result_repr = mark_safe(force_unicode(bf.errors) + force_unicode(bf)) else: result_repr = conditional_escape(result_repr) yield mark_safe(u'%s' % (row_class, result_repr)) - if form: + if form and not form[cl.model._meta.pk.name].is_hidden: yield mark_safe(u'
        ' % force_unicode(form[cl.model._meta.pk.name])) +class ResultList(list): + # Wrapper class used to return items in a list_editable + # changelist, annotated with the form object for error + # reporting purposes. Needed to maintain backwards + # compatibility with existing admin templates. + def __init__(self, form, *items): + self.form = form + super(ResultList, self).__init__(*items) + def results(cl): if cl.formset: for res, form in zip(cl.result_list, cl.formset.forms): - yield list(items_for_result(cl, res, form)) + yield ResultList(form, items_for_result(cl, res, form)) else: for res in cl.result_list: - yield list(items_for_result(cl, res, None)) + yield ResultList(None, items_for_result(cl, res, None)) + +def result_hidden_fields(cl): + if cl.formset: + for res, form in zip(cl.result_list, cl.formset.forms): + if form[cl.model._meta.pk.name].is_hidden: + yield mark_safe(force_unicode(form[cl.model._meta.pk.name])) def result_list(cl): """ Displays the headers and data list together """ return {'cl': cl, + 'result_hidden_fields': list(result_hidden_fields(cl)), 'result_headers': list(result_headers(cl)), 'results': list(results(cl))} result_list = register.inclusion_tag("admin/change_list_results.html")(result_list) @@ -241,7 +265,7 @@ def date_hierarchy(cl): 'show': True, 'back': { 'link': link({year_field: year_lookup}), - 'title': year_lookup + 'title': str(year_lookup) }, 'choices': [{ 'link': link({year_field: year_lookup, month_field: month_lookup, day_field: day.day}), diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index fe88043c9f51..8e6f0ab5cedf 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -21,7 +21,7 @@ def prepopulated_fields_js(context): def submit_row(context): """ - Displays the row of buttons for delete and save. + Displays the row of buttons for delete and save. """ opts = context['opts'] change = context['change'] @@ -33,10 +33,24 @@ def submit_row(context): 'show_delete_link': (not is_popup and context['has_delete_permission'] and (change or context['show_delete'])), 'show_save_as_new': not is_popup and change and save_as, - 'show_save_and_add_another': context['has_add_permission'] and + 'show_save_and_add_another': context['has_add_permission'] and not is_popup and (not save_as or context['add']), 'show_save_and_continue': not is_popup and context['has_change_permission'], 'is_popup': is_popup, 'show_save': True } submit_row = register.inclusion_tag('admin/submit_line.html', takes_context=True)(submit_row) + +def cell_count(inline_admin_form): + """Returns the number of cells used in a tabular inline""" + count = 1 # Hidden cell with hidden 'id' field + for fieldset in inline_admin_form: + # Loop through all the fields (one per cell) + for line in fieldset: + for field in line: + count += 1 + if inline_admin_form.formset.can_delete: + # Delete checkbox + count += 1 + return count +cell_count = register.filter(cell_count) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 776a6f0f42c8..607916f3c7ef 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -1,5 +1,6 @@ from django.core.exceptions import ObjectDoesNotExist from django.db import models +from django.db.models.related import RelatedObject from django.forms.forms import pretty_name from django.utils import formats from django.utils.html import escape @@ -279,7 +280,11 @@ def lookup_field(name, obj, model_admin=None): def label_for_field(name, model, model_admin=None, return_attr=False): attr = None try: - label = model._meta.get_field_by_name(name)[0].verbose_name + field = model._meta.get_field_by_name(name)[0] + if isinstance(field, RelatedObject): + label = field.opts.verbose_name + else: + label = field.verbose_name except models.FieldDoesNotExist: if name == "__unicode__": label = force_unicode(model._meta.verbose_name) @@ -295,7 +300,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False): else: message = "Unable to lookup '%s' on %s" % (name, model._meta.object_name) if model_admin: - message += " or %s" % (model_admin.__name__,) + message += " or %s" % (model_admin.__class__.__name__,) raise AttributeError(message) if hasattr(attr, "short_description"): @@ -312,6 +317,13 @@ 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 + except models.FieldDoesNotExist: + help_text = "" + return smart_unicode(help_text) + def display_for_field(value, field): from django.contrib.admin.templatetags.admin_list import _boolean_icon diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index bee28912af74..ee92b88d6848 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -45,9 +45,8 @@ def validate(cls, model): 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): - fetch_attr(cls, model, opts, 'list_display_links[%d]' % idx, field) if field not in cls.list_display: - raise ImproperlyConfigured("'%s.list_display_links[%d]'" + raise ImproperlyConfigured("'%s.list_display_links[%d]' " "refers to '%s' which is not defined in 'list_display'." % (cls.__name__, idx, field)) @@ -122,16 +121,7 @@ def validate(cls, model): get_field(cls, model, opts, 'ordering[%d]' % idx, 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: - 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)) + check_readonly_fields(cls, model, opts) # list_select_related = False # save_as = False @@ -192,6 +182,9 @@ def validate_inline(cls, parent, parent_model): "'%s' - this is the foreign key to the parent model " "%s." % (cls.__name__, fk.name, parent_model.__name__)) + if hasattr(cls, "readonly_fields"): + check_readonly_fields(cls, cls.model, cls.model._meta) + def validate_base(cls, model): opts = model._meta @@ -377,3 +370,15 @@ 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'." % (cls.__name__, label, field, 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/decorators.py b/django/contrib/admin/views/decorators.py index afdc07589d7c..82184fc9d3f2 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -60,10 +60,6 @@ def _checklogin(request, *args, **kwargs): users = list(User.objects.filter(email=username)) if len(users) == 1 and users[0].check_password(password): message = _("Your e-mail address is not your username. Try '%s' instead.") % users[0].username - else: - # Either we cannot find the user, or if more than 1 - # we cannot guess which user is the correct one. - message = _("Usernames cannot contain the '@' character.") return _display_login_form(request, message) # The user data is correct; log in the user in and continue. diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 8c09c10ab313..e1c3b73526bc 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,6 +1,7 @@ from django.contrib.admin.filterspecs import FilterSpec from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.util import quote +from django.core.exceptions import SuspiciousOperation from django.core.paginator import Paginator, InvalidPage from django.db import models from django.db.models.query import QuerySet @@ -53,8 +54,6 @@ def __init__(self, request, model, list_display, list_display_links, list_filter self.params = dict(request.GET.items()) if PAGE_VAR in self.params: del self.params[PAGE_VAR] - if TO_FIELD_VAR in self.params: - del self.params[TO_FIELD_VAR] if ERROR_FLAG in self.params: del self.params[ERROR_FLAG] @@ -116,7 +115,7 @@ def get_results(self, request): try: result_list = paginator.page(self.page_num+1).object_list except InvalidPage: - result_list = () + raise IncorrectLookupParameters self.result_count = result_count self.full_result_count = full_result_count @@ -166,7 +165,7 @@ def get_ordering(self): def get_query_set(self): qs = self.root_query_set lookup_params = self.params.copy() # a dictionary of the query string - for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): + for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR): if i in lookup_params: del lookup_params[i] for key, value in lookup_params.items(): @@ -178,14 +177,21 @@ def get_query_set(self): # if key ends with __in, split parameter into separate values if key.endswith('__in'): - lookup_params[key] = value.split(',') + value = value.split(',') + lookup_params[key] = value # if key ends with __isnull, special case '' and false if key.endswith('__isnull'): if value.lower() in ('', 'false'): - lookup_params[key] = False + value = False else: - lookup_params[key] = True + value = True + lookup_params[key] = value + + if not self.model_admin.lookup_allowed(key, value): + raise SuspiciousOperation( + "Filtering by %s not allowed" % key + ) # Apply lookup parameters from the query string. try: @@ -193,7 +199,7 @@ def get_query_set(self): # Naked except! Because we don't have any other way of validating "params". # They might be invalid if the keyword arguments are incorrect, or if the # values are not in the correct type, so we might get FieldError, ValueError, - # ValicationError, or ? from a custom field that raises yet something else + # ValicationError, or ? from a custom field that raises yet something else # when handed impossible data. except: raise IncorrectLookupParameters diff --git a/django/contrib/admin/views/template.py b/django/contrib/admin/views/template.py deleted file mode 100644 index 0b8ed3ea9efa..000000000000 --- a/django/contrib/admin/views/template.py +++ /dev/null @@ -1,79 +0,0 @@ -from django import template, forms -from django.contrib.admin.views.decorators import staff_member_required -from django.template import loader -from django.shortcuts import render_to_response -from django.contrib.sites.models import Site -from django.conf import settings -from django.utils.importlib import import_module -from django.utils.translation import ugettext_lazy as _ -from django.contrib import messages - - -def template_validator(request): - """ - Displays the template validator form, which finds and displays template - syntax errors. - """ - # get a dict of {site_id : settings_module} for the validator - settings_modules = {} - for mod in settings.ADMIN_FOR: - settings_module = import_module(mod) - settings_modules[settings_module.SITE_ID] = settings_module - site_list = Site.objects.in_bulk(settings_modules.keys()).values() - if request.POST: - form = TemplateValidatorForm(settings_modules, site_list, - data=request.POST) - if form.is_valid(): - messages.info(request, 'The template is valid.') - else: - form = TemplateValidatorForm(settings_modules, site_list) - return render_to_response('admin/template_validator.html', { - 'title': 'Template validator', - 'form': form, - }, context_instance=template.RequestContext(request)) -template_validator = staff_member_required(template_validator) - - -class TemplateValidatorForm(forms.Form): - site = forms.ChoiceField(_('site')) - template = forms.CharField( - _('template'), widget=forms.Textarea({'rows': 25, 'cols': 80})) - - def __init__(self, settings_modules, site_list, *args, **kwargs): - self.settings_modules = settings_modules - super(TemplateValidatorForm, self).__init__(*args, **kwargs) - self.fields['site'].choices = [(s.id, s.name) for s in site_list] - - def clean_template(self): - # Get the settings module. If the site isn't set, we don't raise an - # error since the site field will. - try: - site_id = int(self.cleaned_data.get('site', None)) - except (ValueError, TypeError): - return - settings_module = self.settings_modules.get(site_id, None) - if settings_module is None: - return - - # So that inheritance works in the site's context, register a new - # function for "extends" that uses the site's TEMPLATE_DIRS instead. - def new_do_extends(parser, token): - node = loader.do_extends(parser, token) - node.template_dirs = settings_module.TEMPLATE_DIRS - return node - register = template.Library() - register.tag('extends', new_do_extends) - template.builtins.append(register) - - # Now validate the template using the new TEMPLATE_DIRS, making sure to - # reset the extends function in any case. - error = None - template_string = self.cleaned_data['template'] - try: - tmpl = loader.get_template_from_string(template_string) - tmpl.render(template.Context({})) - except template.TemplateSyntaxError, e: - error = e - template.builtins.remove(register) - if error: - raise forms.ValidationError(e.args) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 1d321d0620a7..472f69dcf003 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -96,10 +96,30 @@ def render(self, name, value, attrs=None): output = [] if value and hasattr(value, "url"): output.append('%s %s
        %s ' % \ - (_('Currently:'), value.url, value, _('Change:'))) + (_('Currently:'), escape(value.url), escape(value), _('Change:'))) output.append(super(AdminFileWidget, self).render(name, value, attrs)) return mark_safe(u''.join(output)) +def url_params_from_lookup_dict(lookups): + """ + Converts the type of lookups specified in a ForeignKey limit_choices_to + attribute to a dictionary of query parameters + """ + params = {} + if lookups and hasattr(lookups, 'items'): + items = [] + for k, v in lookups.items(): + if isinstance(v, list): + v = u','.join([str(x) for x in v]) + elif isinstance(v, bool): + # See django.db.fields.BooleanField.get_prep_lookup + v = ('0', '1')[v] + else: + v = unicode(v) + items.append((k, v)) + params.update(dict(items)) + return params + class ForeignKeyRawIdWidget(forms.TextInput): """ A Widget for displaying ForeignKeys in the "raw_id" interface rather than @@ -116,33 +136,23 @@ def render(self, name, value, attrs=None): related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower()) params = self.url_parameters() if params: - url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) + url = u'?' + u'&'.join([u'%s=%s' % (k, v) for k, v in params.items()]) else: - url = '' + url = u'' if not attrs.has_key('class'): attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook. output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] # TODO: "id_" is hard-coded here. This should instead use the correct # API to determine the ID dynamically. - output.append(' ' % \ + output.append(u' ' % \ (related_url, url, name)) - output.append('%s' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) + output.append(u'%s' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup'))) if value: output.append(self.label_for_value(value)) return mark_safe(u''.join(output)) def base_url_parameters(self): - params = {} - if self.rel.limit_choices_to and hasattr(self.rel.limit_choices_to, 'items'): - items = [] - for k, v in self.rel.limit_choices_to.items(): - if isinstance(v, list): - v = ','.join([str(x) for x in v]) - else: - v = str(v) - items.append((k, v)) - params.update(dict(items)) - return params + return url_params_from_lookup_dict(self.rel.limit_choices_to) def url_parameters(self): from django.contrib.admin.views.main import TO_FIELD_VAR @@ -154,22 +164,21 @@ def label_for_value(self, value): key = self.rel.get_related_field().name try: obj = self.rel.to._default_manager.using(self.db).get(**{key: value}) - except self.rel.to.DoesNotExist: + return ' %s' % escape(truncate_words(obj, 14)) + except (ValueError, self.rel.to.DoesNotExist): return '' - return ' %s' % escape(truncate_words(obj, 14)) class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): """ A Widget for displaying ManyToMany ids in the "raw_id" interface rather than in a {% endif %} + {% if next %}
        {% endif %}

        or cancel

        diff --git a/django/contrib/comments/templates/comments/base.html b/django/contrib/comments/templates/comments/base.html index 36fc66f7d1f7..c8b951749025 100644 --- a/django/contrib/comments/templates/comments/base.html +++ b/django/contrib/comments/templates/comments/base.html @@ -1,7 +1,7 @@ - - + + - + {% block title %}{% endblock %} diff --git a/django/contrib/comments/templates/comments/delete.html b/django/contrib/comments/templates/comments/delete.html index 5ff2add9c55e..50c9a4d1b18a 100644 --- a/django/contrib/comments/templates/comments/delete.html +++ b/django/contrib/comments/templates/comments/delete.html @@ -7,7 +7,7 @@

        {% trans "Really remove this comment?" %}

        {{ comment|linebreaks }}
        {% csrf_token %} - {% if next %}{% endif %} + {% if next %}
        {% endif %}

        or cancel

        diff --git a/django/contrib/comments/templates/comments/flag.html b/django/contrib/comments/templates/comments/flag.html index 0b9ab1ccb228..ca7c77f111be 100644 --- a/django/contrib/comments/templates/comments/flag.html +++ b/django/contrib/comments/templates/comments/flag.html @@ -7,7 +7,7 @@

        {% trans "Really flag this comment?" %}

        {{ comment|linebreaks }}
        {% csrf_token %} - {% if next %}{% endif %} + {% if next %}
        {% endif %}

        or cancel

        diff --git a/django/contrib/comments/templates/comments/form.html b/django/contrib/comments/templates/comments/form.html index 30f031128c74..2a9ad557dd88 100644 --- a/django/contrib/comments/templates/comments/form.html +++ b/django/contrib/comments/templates/comments/form.html @@ -1,9 +1,9 @@ {% load comments i18n %} {% csrf_token %} - {% if next %}{% endif %} + {% if next %}
        {% endif %} {% for field in form %} {% if field.is_hidden %} - {{ field }} +
        {{ field }}
        {% else %} {% if field.errors %}{{ field.errors }}{% endif %}

        {% csrf_token %} - {% if next %}{% endif %} + {% if next %}

        {% endif %} {% if form.errors %}

        {% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}

        {% else %} @@ -18,7 +18,7 @@

        {% trans "Preview your comment" %}

        {% endif %} {% for field in form %} {% if field.is_hidden %} - {{ field }} +
        {{ field }}
        {% else %} {% if field.errors %}{{ field.errors }}{% endif %}

        >> from django.conf import settings - >>> settings.DEBUG = True +class ContentTypesTests(TestCase): - >>> from django.contrib.contenttypes.models import ContentType - >>> ContentType.objects.clear_cache() + def setUp(self): + # First, let's make sure we're dealing with a blank slate (and that + # DEBUG is on so that queries get logged) + self.old_DEBUG = settings.DEBUG + self.old_Site_meta_installed = Site._meta.installed + settings.DEBUG = True + ContentType.objects.clear_cache() + db.reset_queries() - >>> from django import db - >>> db.reset_queries() - -At this point, a lookup for a ContentType should hit the DB:: + def tearDown(self): + settings.DEBUG = self.old_DEBUG + Site._meta.installed = self.old_Site_meta_installed + ContentType.objects.clear_cache() - >>> ContentType.objects.get_for_model(ContentType) - - - >>> len(db.connection.queries) - 1 + def test_lookup_cache(self): + """ + Make sure that the content type cache (see ContentTypeManager) + works correctly. Lookups for a particular content type -- by model or + by ID -- should hit the database only on the first lookup. + """ -A second hit, though, won't hit the DB, nor will a lookup by ID:: + # At this point, a lookup for a ContentType should hit the DB + ContentType.objects.get_for_model(ContentType) + self.assertEqual(1, len(db.connection.queries)) - >>> ct = ContentType.objects.get_for_model(ContentType) - >>> len(db.connection.queries) - 1 - >>> ContentType.objects.get_for_id(ct.id) - - >>> len(db.connection.queries) - 1 + # A second hit, though, won't hit the DB, nor will a lookup by ID + ct = ContentType.objects.get_for_model(ContentType) + self.assertEqual(1, len(db.connection.queries)) + ContentType.objects.get_for_id(ct.id) + self.assertEqual(1, len(db.connection.queries)) -Once we clear the cache, another lookup will again hit the DB:: + # Once we clear the cache, another lookup will again hit the DB + ContentType.objects.clear_cache() + ContentType.objects.get_for_model(ContentType) + len(db.connection.queries) + self.assertEqual(2, len(db.connection.queries)) - >>> ContentType.objects.clear_cache() - >>> ContentType.objects.get_for_model(ContentType) - - >>> len(db.connection.queries) - 2 + def test_shortcut_view(self): + """ + Check that the shortcut view (used for the admin "view on site" + functionality) returns a complete URL regardless of whether the sites + framework is installed + """ -Don't forget to reset DEBUG! + request = HttpRequest() + request.META = { + "SERVER_NAME": "Example.com", + "SERVER_PORT": "80", + } + from django.contrib.auth.models import User + user_ct = ContentType.objects.get_for_model(User) + obj = User.objects.create(username="john") - >>> settings.DEBUG = False -""" \ No newline at end of file + if Site._meta.installed: + current_site = Site.objects.get_current() + response = shortcut(request, user_ct.id, obj.id) + self.assertEqual("http://%s/users/john/" % current_site.domain, + response._headers.get("location")[1]) + + Site._meta.installed = False + response = shortcut(request, user_ct.id, obj.id) + self.assertEqual("http://Example.com/users/john/", + response._headers.get("location")[1]) diff --git a/django/contrib/contenttypes/views.py b/django/contrib/contenttypes/views.py index 26961201cd17..ac0feffe7a0d 100644 --- a/django/contrib/contenttypes/views.py +++ b/django/contrib/contenttypes/views.py @@ -1,6 +1,6 @@ from django import http from django.contrib.contenttypes.models import ContentType -from django.contrib.sites.models import Site +from django.contrib.sites.models import Site, get_current_site from django.core.exceptions import ObjectDoesNotExist def shortcut(request, content_type_id, object_id): @@ -8,6 +8,8 @@ def shortcut(request, content_type_id, object_id): # Look up the object, making sure it's got a get_absolute_url() function. try: content_type = ContentType.objects.get(pk=content_type_id) + if not content_type.model_class(): + raise http.Http404("Content type %s object has no associated model" % content_type_id) obj = content_type.get_object_for_this_type(pk=object_id) except (ObjectDoesNotExist, ValueError): raise http.Http404("Content type %s object %s doesn't exist" % (content_type_id, object_id)) @@ -26,35 +28,37 @@ def shortcut(request, content_type_id, object_id): # Otherwise, we need to introspect the object's relationships for a # relation to the Site object object_domain = None - opts = obj._meta - # First, look for an many-to-many relationship to Site. - for field in opts.many_to_many: - if field.rel.to is Site: - try: - # Caveat: In the case of multiple related Sites, this just - # selects the *first* one, which is arbitrary. - object_domain = getattr(obj, field.name).all()[0].domain - except IndexError: - pass - if object_domain is not None: - break + if Site._meta.installed: + opts = obj._meta - # Next, look for a many-to-one relationship to Site. - if object_domain is None: - for field in obj._meta.fields: - if field.rel and field.rel.to is Site: + # First, look for an many-to-many relationship to Site. + for field in opts.many_to_many: + if field.rel.to is Site: try: - object_domain = getattr(obj, field.name).domain - except Site.DoesNotExist: + # Caveat: In the case of multiple related Sites, this just + # selects the *first* one, which is arbitrary. + object_domain = getattr(obj, field.name).all()[0].domain + except IndexError: pass if object_domain is not None: break + # Next, look for a many-to-one relationship to Site. + if object_domain is None: + for field in obj._meta.fields: + if field.rel and field.rel.to is Site: + try: + object_domain = getattr(obj, field.name).domain + except Site.DoesNotExist: + pass + if object_domain is not None: + break + # Fall back to the current site (if possible). if object_domain is None: try: - object_domain = Site.objects.get_current().domain + object_domain = get_current_site(request).domain except Site.DoesNotExist: pass diff --git a/django/contrib/databrowse/templates/databrowse/base.html b/django/contrib/databrowse/templates/databrowse/base.html index a3419851c43a..33cac486014d 100644 --- a/django/contrib/databrowse/templates/databrowse/base.html +++ b/django/contrib/databrowse/templates/databrowse/base.html @@ -1,5 +1,5 @@ - + {% block title %}{% endblock %} {% block style %} diff --git a/django/contrib/flatpages/fixtures/sample_flatpages.json b/django/contrib/flatpages/fixtures/sample_flatpages.json new file mode 100644 index 000000000000..79808c2d2b62 --- /dev/null +++ b/django/contrib/flatpages/fixtures/sample_flatpages.json @@ -0,0 +1,32 @@ +[ + { + "pk": 1, + "model": "flatpages.flatpage", + "fields": { + "registration_required": false, + "title": "A Flatpage", + "url": "/flatpage/", + "template_name": "", + "sites": [ + 1 + ], + "content": "Isn't it flat!", + "enable_comments": false + } + }, + { + "pk": 2, + "model": "flatpages.flatpage", + "fields": { + "registration_required": true, + "title": "Sekrit Flatpage", + "url": "/sekrit/", + "template_name": "", + "sites": [ + 1 + ], + "content": "Isn't it sekrit!", + "enable_comments": false + } + } +] \ No newline at end of file diff --git a/django/contrib/flatpages/tests/__init__.py b/django/contrib/flatpages/tests/__init__.py new file mode 100644 index 000000000000..2672dbfae80c --- /dev/null +++ b/django/contrib/flatpages/tests/__init__.py @@ -0,0 +1,3 @@ +from django.contrib.flatpages.tests.csrf import * +from django.contrib.flatpages.tests.middleware import * +from django.contrib.flatpages.tests.views import * diff --git a/django/contrib/flatpages/tests/csrf.py b/django/contrib/flatpages/tests/csrf.py new file mode 100644 index 000000000000..0f0ab08ed2ae --- /dev/null +++ b/django/contrib/flatpages/tests/csrf.py @@ -0,0 +1,73 @@ +import os +from django.conf import settings +from django.test import TestCase, Client + +class FlatpageCSRFTests(TestCase): + fixtures = ['sample_flatpages'] + urls = 'django.contrib.flatpages.tests.urls' + + def setUp(self): + self.client = Client(enforce_csrf_checks=True) + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + flatpage_middleware_class = 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' + csrf_middleware_class = 'django.middleware.csrf.CsrfViewMiddleware' + if csrf_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (csrf_middleware_class,) + if flatpage_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (flatpage_middleware_class,) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + self.old_LOGIN_URL = settings.LOGIN_URL + settings.LOGIN_URL = '/accounts/login/' + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + settings.LOGIN_URL = self.old_LOGIN_URL + + def test_view_flatpage(self): + "A flatpage can be served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "

        Isn't it flat!

        ") + + def test_view_non_existent_flatpage(self): + "A non-existent flatpage raises 404 when served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_view_authenticated_flatpage(self): + "A flatpage served through a view can require authentication" + response = self.client.get('/flatpage_root/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/flatpage_root/sekrit/') + + def test_fallback_flatpage(self): + "A flatpage can be served by the fallback middlware" + response = self.client.get('/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "

        Isn't it flat!

        ") + + def test_fallback_non_existent_flatpage(self): + "A non-existent flatpage raises a 404 when served by the fallback middlware" + response = self.client.get('/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_post_view_flatpage(self): + "POSTing to a flatpage served through a view will raise a CSRF error if no token is provided (Refs #14156)" + response = self.client.post('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 403) + + def test_post_fallback_flatpage(self): + "POSTing to a flatpage served by the middleware will raise a CSRF error if no token is provided (Refs #14156)" + response = self.client.post('/flatpage/') + self.assertEquals(response.status_code, 403) + + def test_post_unknown_page(self): + "POSTing to an unknown page isn't caught as a 403 CSRF error" + response = self.client.post('/no_such_page/') + self.assertEquals(response.status_code, 404) diff --git a/django/contrib/flatpages/tests/middleware.py b/django/contrib/flatpages/tests/middleware.py new file mode 100644 index 000000000000..a41259626d24 --- /dev/null +++ b/django/contrib/flatpages/tests/middleware.py @@ -0,0 +1,59 @@ +import os +from django.conf import settings +from django.test import TestCase + +class FlatpageMiddlewareTests(TestCase): + fixtures = ['sample_flatpages'] + urls = 'django.contrib.flatpages.tests.urls' + + def setUp(self): + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + flatpage_middleware_class = 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' + if flatpage_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (flatpage_middleware_class,) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + self.old_LOGIN_URL = settings.LOGIN_URL + settings.LOGIN_URL = '/accounts/login/' + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + settings.LOGIN_URL = self.old_LOGIN_URL + + def test_view_flatpage(self): + "A flatpage can be served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "

        Isn't it flat!

        ") + + def test_view_non_existent_flatpage(self): + "A non-existent flatpage raises 404 when served through a view, even when the middleware is in use" + response = self.client.get('/flatpage_root/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_view_authenticated_flatpage(self): + "A flatpage served through a view can require authentication" + response = self.client.get('/flatpage_root/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/flatpage_root/sekrit/') + + def test_fallback_flatpage(self): + "A flatpage can be served by the fallback middlware" + response = self.client.get('/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "

        Isn't it flat!

        ") + + def test_fallback_non_existent_flatpage(self): + "A non-existent flatpage raises a 404 when served by the fallback middlware" + response = self.client.get('/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_fallback_authenticated_flatpage(self): + "A flatpage served by the middleware can require authentication" + response = self.client.get('/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/sekrit/') diff --git a/django/contrib/flatpages/tests/templates/404.html b/django/contrib/flatpages/tests/templates/404.html new file mode 100644 index 000000000000..5fd5f3cf3b6a --- /dev/null +++ b/django/contrib/flatpages/tests/templates/404.html @@ -0,0 +1 @@ +

        Oh Noes!

        \ No newline at end of file diff --git a/django/contrib/flatpages/tests/templates/flatpages/default.html b/django/contrib/flatpages/tests/templates/flatpages/default.html new file mode 100644 index 000000000000..c6323fd91de8 --- /dev/null +++ b/django/contrib/flatpages/tests/templates/flatpages/default.html @@ -0,0 +1,2 @@ +

        {{ flatpage.title }}

        +

        {{ flatpage.content }}

        \ No newline at end of file diff --git a/tests/regressiontests/datastructures/__init__.py b/django/contrib/flatpages/tests/templates/registration/login.html similarity index 100% rename from tests/regressiontests/datastructures/__init__.py rename to django/contrib/flatpages/tests/templates/registration/login.html diff --git a/django/contrib/flatpages/tests/urls.py b/django/contrib/flatpages/tests/urls.py new file mode 100644 index 000000000000..3cffd09d0f99 --- /dev/null +++ b/django/contrib/flatpages/tests/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +# special urls for flatpage test cases +urlpatterns = patterns('', + (r'^flatpage_root', include('django.contrib.flatpages.urls')), + (r'^accounts/', include('django.contrib.auth.urls')), +) + diff --git a/django/contrib/flatpages/tests/views.py b/django/contrib/flatpages/tests/views.py new file mode 100644 index 000000000000..a013ae982513 --- /dev/null +++ b/django/contrib/flatpages/tests/views.py @@ -0,0 +1,53 @@ +import os +from django.conf import settings +from django.test import TestCase + +class FlatpageViewTests(TestCase): + fixtures = ['sample_flatpages'] + urls = 'django.contrib.flatpages.tests.urls' + + def setUp(self): + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + flatpage_middleware_class = 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware' + if flatpage_middleware_class in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES = tuple(m for m in settings.MIDDLEWARE_CLASSES if m != flatpage_middleware_class) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = ( + os.path.join( + os.path.dirname(__file__), + 'templates' + ), + ) + self.old_LOGIN_URL = settings.LOGIN_URL + settings.LOGIN_URL = '/accounts/login/' + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS + settings.LOGIN_URL = self.old_LOGIN_URL + + def test_view_flatpage(self): + "A flatpage can be served through a view" + response = self.client.get('/flatpage_root/flatpage/') + self.assertEquals(response.status_code, 200) + self.assertContains(response, "

        Isn't it flat!

        ") + + def test_view_non_existent_flatpage(self): + "A non-existent flatpage raises 404 when served through a view" + response = self.client.get('/flatpage_root/no_such_flatpage/') + self.assertEquals(response.status_code, 404) + + def test_view_authenticated_flatpage(self): + "A flatpage served through a view can require authentication" + response = self.client.get('/flatpage_root/sekrit/') + self.assertRedirects(response, '/accounts/login/?next=/flatpage_root/sekrit/') + + def test_fallback_flatpage(self): + "A fallback flatpage won't be served if the middleware is disabled" + response = self.client.get('/flatpage/') + self.assertEquals(response.status_code, 404) + + def test_fallback_non_existent_flatpage(self): + "A non-existent flatpage won't be served if the fallback middlware is disabled" + response = self.client.get('/no_such_flatpage/') + self.assertEquals(response.status_code, 404) diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py index 336600328d04..88ef4da65e22 100644 --- a/django/contrib/flatpages/views.py +++ b/django/contrib/flatpages/views.py @@ -13,10 +13,13 @@ # when a 404 is raised, which often means CsrfViewMiddleware.process_view # has not been called even if CsrfViewMiddleware is installed. So we need # to use @csrf_protect, in case the template needs {% csrf_token %}. -@csrf_protect +# However, we can't just wrap this view; if no matching flatpage exists, +# or a redirect is required for authentication, the 404 needs to be returned +# without any CSRF checks. Therefore, we only +# CSRF protect the internal implementation. def flatpage(request, url): """ - Flat page view. + Public interface to the flat page view. Models: `flatpages.flatpages` Templates: Uses the template defined by the ``template_name`` field, @@ -30,6 +33,13 @@ def flatpage(request, url): if not url.startswith('/'): url = "/" + url f = get_object_or_404(FlatPage, url__exact=url, sites__id__exact=settings.SITE_ID) + return render_flatpage(request, f) + +@csrf_protect +def render_flatpage(request, f): + """ + Internal interface to the flat page view. + """ # If registration is required for accessing this page, and the user isn't # logged in, redirect to the login page. if f.registration_required and not request.user.is_authenticated(): diff --git a/django/contrib/formtools/tests.py b/django/contrib/formtools/tests.py index bc65a60fbea1..de71fe5443e4 100644 --- a/django/contrib/formtools/tests.py +++ b/django/contrib/formtools/tests.py @@ -1,5 +1,6 @@ import unittest from django import forms +from django.conf import settings from django.contrib.formtools import preview, wizard, utils from django import http from django.test import TestCase @@ -115,7 +116,7 @@ def test_textfield_hash(self): hash1 = utils.security_hash(None, f1) hash2 = utils.security_hash(None, f2) self.assertEqual(hash1, hash2) - + def test_empty_permitted(self): """ Regression test for #10643: the security hash should allow forms with @@ -145,6 +146,12 @@ class WizardPageOneForm(forms.Form): class WizardPageTwoForm(forms.Form): field = forms.CharField() +class WizardPageTwoAlternativeForm(forms.Form): + field = forms.CharField() + +class WizardPageThreeForm(forms.Form): + field = forms.CharField() + class WizardClass(wizard.FormWizard): def render_template(self, *args, **kw): return http.HttpResponse("") @@ -161,6 +168,15 @@ def __init__(self, POST=None): self._dont_enforce_csrf_checks = True class WizardTests(TestCase): + + def setUp(self): + # Use a known SECRET_KEY to make security_hash tests deterministic + self.old_SECRET_KEY = settings.SECRET_KEY + settings.SECRET_KEY = "123" + + def tearDown(self): + settings.SECRET_KEY = self.old_SECRET_KEY + def test_step_starts_at_zero(self): """ step should be zero for the first form @@ -179,3 +195,75 @@ def test_step_increments(self): response = wizard(request) self.assertEquals(1, wizard.step) + def test_14498(self): + """ + Regression test for ticket #14498. All previous steps' forms should be + validated. + """ + that = self + reached = [False] + + class WizardWithProcessStep(WizardClass): + def process_step(self, request, form, step): + reached[0] = True + that.assertTrue(hasattr(form, 'cleaned_data')) + + wizard = WizardWithProcessStep([WizardPageOneForm, + WizardPageTwoForm, + WizardPageThreeForm]) + data = {"0-field": "test", + "1-field": "test2", + "hash_0": "2fdbefd4c0cad51509478fbacddf8b13", + "wizard_step": "1"} + wizard(DummyRequest(POST=data)) + self.assertTrue(reached[0]) + + def test_14576(self): + """ + Regression test for ticket #14576. + + The form of the last step is not passed to the done method. + """ + reached = [False] + that = self + + class Wizard(WizardClass): + def done(self, request, form_list): + reached[0] = True + that.assertTrue(len(form_list) == 2) + + wizard = Wizard([WizardPageOneForm, + WizardPageTwoForm]) + + data = {"0-field": "test", + "1-field": "test2", + "hash_0": "2fdbefd4c0cad51509478fbacddf8b13", + "wizard_step": "1"} + wizard(DummyRequest(POST=data)) + self.assertTrue(reached[0]) + + def test_15075(self): + """ + Regression test for ticket #15075. Allow modifying wizard's form_list + in process_step. + """ + reached = [False] + that = self + + class WizardWithProcessStep(WizardClass): + def process_step(self, request, form, step): + if step == 0: + self.form_list[1] = WizardPageTwoAlternativeForm + if step == 1: + that.assertTrue(isinstance(form, WizardPageTwoAlternativeForm)) + reached[0] = True + + wizard = WizardWithProcessStep([WizardPageOneForm, + WizardPageTwoForm, + WizardPageThreeForm]) + data = {"0-field": "test", + "1-field": "test2", + "hash_0": "2fdbefd4c0cad51509478fbacddf8b13", + "wizard_step": "1"} + wizard(DummyRequest(POST=data)) + self.assertTrue(reached[0]) diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py index 02d8fd71d42c..3e4e036dffd3 100644 --- a/django/contrib/formtools/wizard.py +++ b/django/contrib/formtools/wizard.py @@ -27,7 +27,7 @@ class FormWizard(object): def __init__(self, form_list, initial=None): """ Start a new wizard with a list of forms. - + form_list should be a list of Form classes (not instances). """ self.form_list = form_list[:] @@ -37,7 +37,7 @@ def __init__(self, form_list, initial=None): self.extra_context = {} # A zero-based counter keeping track of which step we're in. - self.step = 0 + self.step = 0 def __repr__(self): return "step: %d\nform_list: %s\ninitial_data: %s" % (self.step, self.form_list, self.initial) @@ -48,7 +48,7 @@ def get_form(self, step, data=None): def num_steps(self): "Helper method that returns the number of steps." - # You might think we should just set "self.form_list = len(form_list)" + # You might think we should just set "self.num_steps = len(form_list)" # in __init__(), but this calculation needs to be dynamic, because some # hook methods might alter self.form_list. return len(self.form_list) @@ -68,13 +68,38 @@ def __call__(self, request, *args, **kwargs): if current_step >= self.num_steps(): raise Http404('Step %s does not exist' % current_step) - # For each previous step, verify the hash and process. - # TODO: Move "hash_%d" to a method to make it configurable. + # Validate and process all the previous forms before instantiating the + # current step's form in case self.process_step makes changes to + # self.form_list. + + # If any of them fails validation, that must mean the validator relied + # on some other input, such as an external Web site. + + # It is also possible that alidation might fail under certain attack + # situations: an attacker might be able to bypass previous stages, and + # generate correct security hashes for all the skipped stages by virtue + # of: + # 1) having filled out an identical form which doesn't have the + # validation (and does something different at the end), + # 2) or having filled out a previous version of the same form which + # had some validation missing, + # 3) or previously having filled out the form when they had more + # privileges than they do now. + # + # Since the hashes only take into account values, and not other other + # validation the form might do, we must re-do validation now for + # security reasons. + previous_form_list = [] for i in range(current_step): - form = self.get_form(i, request.POST) - if request.POST.get("hash_%d" % i, '') != self.security_hash(request, form): + f = self.get_form(i, request.POST) + if request.POST.get("hash_%d" % i, '') != self.security_hash(request, f): return self.render_hash_failure(request, i) - self.process_step(request, form, i) + + if not f.is_valid(): + return self.render_revalidation_failure(request, i, f) + else: + self.process_step(request, f, i) + previous_form_list.append(f) # Process the current step. If it's valid, go to the next step or call # done(), depending on whether any steps remain. @@ -82,25 +107,14 @@ def __call__(self, request, *args, **kwargs): form = self.get_form(current_step, request.POST) else: form = self.get_form(current_step) + if form.is_valid(): self.process_step(request, form, current_step) next_step = current_step + 1 - # If this was the last step, validate all of the forms one more - # time, as a sanity check, and call done(). - num = self.num_steps() - if next_step == num: - final_form_list = [self.get_form(i, request.POST) for i in range(num)] - - # Validate all the forms. If any of them fail validation, that - # must mean the validator relied on some other input, such as - # an external Web site. - for i, f in enumerate(final_form_list): - if not f.is_valid(): - return self.render_revalidation_failure(request, i, f) - return self.done(request, final_form_list) - - # Otherwise, move along to the next step. + + if next_step == self.num_steps(): + return self.done(request, previous_form_list + [form]) else: form = self.get_form(next_step) self.step = current_step = next_step diff --git a/django/contrib/gis/admin/options.py b/django/contrib/gis/admin/options.py index a1da28108d73..62c45e7220ff 100644 --- a/django/contrib/gis/admin/options.py +++ b/django/contrib/gis/admin/options.py @@ -113,12 +113,19 @@ class OLMap(self.widget): from django.contrib.gis import gdal if gdal.HAS_GDAL: + # Use the official spherical mercator projection SRID on versions + # of GDAL that support it; otherwise, fallback to 900913 + if gdal.GDAL_VERSION >= (1, 7): + spherical_mercator_srid = 3857 + else: + spherical_mercator_srid = 900913 + class OSMGeoAdmin(GeoModelAdmin): map_template = 'gis/admin/osm.html' extra_js = ['http://openstreetmap.org/openlayers/OpenStreetMap.js'] num_zoom = 20 - map_srid = 900913 + map_srid = spherical_mercator_srid max_extent = '-20037508,-20037508,20037508,20037508' - max_resolution = 156543.0339 + max_resolution = '156543.0339' point_zoom = num_zoom - 6 units = 'm' diff --git a/django/contrib/gis/db/backends/mysql/creation.py b/django/contrib/gis/db/backends/mysql/creation.py index 93fd2e6166c2..dda77ea6abd3 100644 --- a/django/contrib/gis/db/backends/mysql/creation.py +++ b/django/contrib/gis/db/backends/mysql/creation.py @@ -6,7 +6,7 @@ def sql_indexes_for_field(self, model, f, style): from django.contrib.gis.db.models.fields import GeometryField output = super(MySQLCreation, self).sql_indexes_for_field(model, f, style) - if isinstance(f, GeometryField): + if isinstance(f, GeometryField) and f.spatial_index: qn = self.connection.ops.quote_name db_table = model._meta.db_table idx_name = '%s_%s_id' % (db_table, f.column) diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index b1a9e315292e..5affcf9a189a 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -233,8 +233,6 @@ def get_dist_ops(operator): }) self.geography_operators = { 'bboverlaps' : PostGISOperator('&&'), - 'exact' : PostGISOperator('~='), - 'same_as' : PostGISOperator('~='), } # Creating a dictionary lookup of all GIS terms for PostGIS. diff --git a/django/contrib/gis/db/backends/spatialite/base.py b/django/contrib/gis/db/backends/spatialite/base.py index d419dab5e122..729fc152e7f2 100644 --- a/django/contrib/gis/db/backends/spatialite/base.py +++ b/django/contrib/gis/db/backends/spatialite/base.py @@ -51,7 +51,7 @@ def _cursor(self): self.connection.create_function("django_extract", 2, _sqlite_extract) self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) self.connection.create_function("regexp", 2, _sqlite_regexp) - connection_created.send(sender=self.__class__) + connection_created.send(sender=self.__class__, connection=self) ## From here on, customized for GeoDjango ## diff --git a/django/contrib/gis/db/backends/util.py b/django/contrib/gis/db/backends/util.py index ed35ce724a5f..b50c8e222e8d 100644 --- a/django/contrib/gis/db/backends/util.py +++ b/django/contrib/gis/db/backends/util.py @@ -3,20 +3,6 @@ backends. """ -def getstatusoutput(cmd): - """ - Executes a shell command on the platform using subprocess.Popen and - return a tuple of the status and stdout output. - """ - from subprocess import Popen, PIPE - # Set stdout and stderr to PIPE because we want to capture stdout and - # prevent stderr from displaying. - p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) - # We use p.communicate() instead of p.wait() to avoid deadlocks if the - # output buffers exceed POSIX buffer size. - stdout, stderr = p.communicate() - return p.returncode, stdout.strip() - def gqn(val): """ The geographic quote name function; used for quoting tables and diff --git a/django/contrib/gis/db/models/query.py b/django/contrib/gis/db/models/query.py index 79eed853f9d1..4df1a3ab7852 100644 --- a/django/contrib/gis/db/models/query.py +++ b/django/contrib/gis/db/models/query.py @@ -48,7 +48,10 @@ def area(self, tolerance=0.05, **kwargs): s['procedure_args']['tolerance'] = tolerance s['select_field'] = AreaField('sq_m') # Oracle returns area in units of meters. elif backend.postgis or backend.spatialite: - if not geo_field.geodetic(connection): + if backend.geography: + # Geography fields support area calculation, returns square meters. + s['select_field'] = AreaField('sq_m') + elif not geo_field.geodetic(connection): # Getting the area units of the geographic field. s['select_field'] = AreaField(Area.unit_attname(geo_field.units_name(connection))) else: diff --git a/django/contrib/gis/db/models/sql/compiler.py b/django/contrib/gis/db/models/sql/compiler.py index 78eeeafe190a..dea0fd3e8300 100644 --- a/django/contrib/gis/db/models/sql/compiler.py +++ b/django/contrib/gis/db/models/sql/compiler.py @@ -56,7 +56,7 @@ def get_columns(self, with_aliases=False): col_aliases.add(col[1]) else: result.append(col.as_sql(qn, self.connection)) - + if hasattr(col, 'alias'): aliases.add(col.alias) col_aliases.add(col.alias) @@ -95,7 +95,7 @@ def get_columns(self, with_aliases=False): return result def get_default_columns(self, with_aliases=False, col_aliases=None, - start_alias=None, opts=None, as_pairs=False): + start_alias=None, opts=None, as_pairs=False, local_only=False): """ Computes the default columns for selecting every field in the base model. Will sometimes be called to pull in related models (e.g. via @@ -121,6 +121,8 @@ def get_default_columns(self, with_aliases=False, col_aliases=None, if start_alias: seen = {None: start_alias} for field, model in opts.get_fields_with_model(): + if local_only and model is not None: + continue if start_alias: try: alias = seen[model] diff --git a/django/contrib/gis/gdal/libgdal.py b/django/contrib/gis/gdal/libgdal.py index 6589c5647c27..256ddb24edc3 100644 --- a/django/contrib/gis/gdal/libgdal.py +++ b/django/contrib/gis/gdal/libgdal.py @@ -13,11 +13,11 @@ if lib_path: lib_names = None elif os.name == 'nt': - # Windows NT shared library - lib_names = ['gdal16', 'gdal15'] + # Windows NT shared libraries + lib_names = ['gdal18', 'gdal17', 'gdal16', 'gdal15'] elif os.name == 'posix': # *NIX library names. - lib_names = ['gdal', 'GDAL', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.0'] + lib_names = ['gdal', 'GDAL', 'gdal1.7.0', 'gdal1.6.0', 'gdal1.5.0', 'gdal1.4.0'] else: raise OGRException('Unsupported OS "%s"' % os.name) diff --git a/django/contrib/gis/gdal/srs.py b/django/contrib/gis/gdal/srs.py index 93bd8416ff6c..95e71f1e31d3 100644 --- a/django/contrib/gis/gdal/srs.py +++ b/django/contrib/gis/gdal/srs.py @@ -37,7 +37,7 @@ #### Spatial Reference class. #### class SpatialReference(GDALBase): """ - A wrapper for the OGRSpatialReference object. According to the GDAL website, + A wrapper for the OGRSpatialReference object. According to the GDAL Web site, the SpatialReference object "provide[s] services to represent coordinate systems (projections and datums) and to transform between them." """ diff --git a/django/contrib/gis/gdal/tests/test_ds.py b/django/contrib/gis/gdal/tests/test_ds.py index 1abea785cacc..e1083b2a35d3 100644 --- a/django/contrib/gis/gdal/tests/test_ds.py +++ b/django/contrib/gis/gdal/tests/test_ds.py @@ -1,20 +1,7 @@ import os, os.path, unittest -from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRException, OGRIndexError +from django.contrib.gis.gdal import DataSource, Envelope, OGRGeometry, OGRException, OGRIndexError, GDAL_VERSION from django.contrib.gis.gdal.field import OFTReal, OFTInteger, OFTString -from django.contrib import gis - -# Path for SHP files -data_path = os.path.join(os.path.dirname(gis.__file__), 'tests' + os.sep + 'data') -def get_ds_file(name, ext): - return os.sep.join([data_path, name, name + '.%s' % ext]) - -# Test SHP data source object -class TestDS: - def __init__(self, name, **kwargs): - ext = kwargs.pop('ext', 'shp') - self.ds = get_ds_file(name, ext) - for key, value in kwargs.items(): - setattr(self, key, value) +from django.contrib.gis.geometry.test_data import get_ds_file, TestDS # List of acceptable data sources. ds_list = (TestDS('test_point', nfeat=5, nfld=3, geom='POINT', gtype=1, driver='ESRI Shapefile', @@ -28,7 +15,7 @@ def __init__(self, name, **kwargs): extent=(1.0, 2.0, 100.0, 523.5), # Min/Max from CSV field_values={'POINT_X' : ['1.0', '5.0', '100.0'], 'POINT_Y' : ['2.0', '23.0', '523.5'], 'NUM' : ['5', '17', '23']}, fids=range(1,4)), - TestDS('test_poly', nfeat=3, nfld=3, geom='POLYGON', gtype=3, + TestDS('test_poly', nfeat=3, nfld=3, geom='POLYGON', gtype=3, driver='ESRI Shapefile', fields={'float' : OFTReal, 'int' : OFTInteger, 'str' : OFTString,}, extent=(-1.01513,-0.558245,0.161876,0.839637), # Got extent from QGIS @@ -63,7 +50,7 @@ def test01_valid_shp(self): pass else: self.fail('Expected an IndexError!') - + def test02_invalid_shp(self): "Testing invalid SHP files for the Data Source." for source in bad_ds: @@ -76,7 +63,7 @@ def test03a_layers(self): ds = DataSource(source.ds) # Incrementing through each layer, this tests DataSource.__iter__ - for layer in ds: + for layer in ds: # Making sure we get the number of features we expect self.assertEqual(len(layer), source.nfeat) @@ -85,16 +72,22 @@ def test03a_layers(self): self.assertEqual(source.nfld, len(layer.fields)) # Testing the layer's extent (an Envelope), and it's properties - self.assertEqual(True, isinstance(layer.extent, Envelope)) - self.assertAlmostEqual(source.extent[0], layer.extent.min_x, 5) - self.assertAlmostEqual(source.extent[1], layer.extent.min_y, 5) - self.assertAlmostEqual(source.extent[2], layer.extent.max_x, 5) - self.assertAlmostEqual(source.extent[3], layer.extent.max_y, 5) + if source.driver == 'VRT' and (GDAL_VERSION > (1, 7, 0) and GDAL_VERSION < (1, 7, 3)): + # There's a known GDAL regression with retrieving the extent + # of a VRT layer in versions 1.7.0-1.7.2: + # http://trac.osgeo.org/gdal/ticket/3783 + pass + else: + self.assertEqual(True, isinstance(layer.extent, Envelope)) + self.assertAlmostEqual(source.extent[0], layer.extent.min_x, 5) + self.assertAlmostEqual(source.extent[1], layer.extent.min_y, 5) + self.assertAlmostEqual(source.extent[2], layer.extent.max_x, 5) + self.assertAlmostEqual(source.extent[3], layer.extent.max_y, 5) # Now checking the field names. flds = layer.fields for f in flds: self.assertEqual(True, f in source.fields) - + # Negative FIDs are not allowed. self.assertRaises(OGRIndexError, layer.__getitem__, -1) self.assertRaises(OGRIndexError, layer.__getitem__, 50000) @@ -115,7 +108,7 @@ def test03a_layers(self): for fld_name in fld_names: self.assertEqual(source.field_values[fld_name][i], feat.get(fld_name)) print "\nEND - expecting out of range feature id error; safe to ignore." - + def test03b_layer_slice(self): "Test indexing and slicing on Layers." # Using the first data-source because the same slice @@ -146,7 +139,7 @@ def get_layer(): # Making sure we can call OGR routines on the Layer returned. lyr = get_layer() self.assertEqual(source.nfeat, len(lyr)) - self.assertEqual(source.gtype, lyr.geom_type.num) + self.assertEqual(source.gtype, lyr.geom_type.num) def test04_features(self): "Testing Data Source Features." @@ -170,7 +163,7 @@ def test04_features(self): # Testing Feature.__iter__ for fld in feat: self.assertEqual(True, fld.name in source.fields.keys()) - + def test05_geometries(self): "Testing Geometries from Data Source Features." for source in ds_list: @@ -223,7 +216,7 @@ def test06_spatial_filter(self): # should indicate that there are 3 features in the Layer. lyr.spatial_filter = None self.assertEqual(3, len(lyr)) - + def suite(): s = unittest.TestSuite() s.addTest(unittest.makeSuite(DataSourceTest)) diff --git a/django/contrib/gis/gdal/tests/test_geom.py b/django/contrib/gis/gdal/tests/test_geom.py index 5aa1f244ae8c..4e1735e48b9d 100644 --- a/django/contrib/gis/gdal/tests/test_geom.py +++ b/django/contrib/gis/gdal/tests/test_geom.py @@ -1,10 +1,10 @@ import unittest from django.contrib.gis.gdal import OGRGeometry, OGRGeomType, \ OGRException, OGRIndexError, SpatialReference, CoordTransform, \ - gdal_version -from django.contrib.gis.tests.geometries import * + GDAL_VERSION +from django.contrib.gis.geometry.test_data import TestDataMixin -class OGRGeomTest(unittest.TestCase): +class OGRGeomTest(unittest.TestCase, TestDataMixin): "This tests the OGR Geometry." def test00a_geomtype(self): @@ -55,7 +55,7 @@ def test00b_geomtype_25d(self): def test01a_wkt(self): "Testing WKT output." - for g in wkt_out: + for g in self.geometries.wkt_out: geom = OGRGeometry(g.wkt) self.assertEqual(g.wkt, geom.wkt) @@ -72,13 +72,18 @@ def test01a_ewkt(self): def test01b_gml(self): "Testing GML output." - for g in wkt_out: + for g in self.geometries.wkt_out: geom = OGRGeometry(g.wkt) - self.assertEqual(g.gml, geom.gml) + exp_gml = g.gml + if GDAL_VERSION >= (1, 8): + # In GDAL 1.8, the non-conformant GML tag was + # replaced with . + exp_gml = exp_gml.replace('GeometryCollection', 'MultiGeometry') + self.assertEqual(exp_gml, geom.gml) def test01c_hex(self): "Testing HEX input/output." - for g in hex_wkt: + for g in self.geometries.hex_wkt: geom1 = OGRGeometry(g.wkt) self.assertEqual(g.hex, geom1.hex) # Constructing w/HEX @@ -88,7 +93,7 @@ def test01c_hex(self): def test01d_wkb(self): "Testing WKB input/output." from binascii import b2a_hex - for g in hex_wkt: + for g in self.geometries.hex_wkt: geom1 = OGRGeometry(g.wkt) wkb = geom1.wkb self.assertEqual(b2a_hex(wkb).upper(), g.hex) @@ -100,7 +105,7 @@ def test01e_json(self): "Testing GeoJSON input/output." from django.contrib.gis.gdal.prototypes.geom import GEOJSON if not GEOJSON: return - for g in json_geoms: + for g in self.geometries.json_geoms: geom = OGRGeometry(g.wkt) if not hasattr(g, 'not_equal'): self.assertEqual(g.json, geom.json) @@ -111,7 +116,7 @@ def test02_points(self): "Testing Point objects." prev = OGRGeometry('POINT(0 0)') - for p in points: + for p in self.geometries.points: if not hasattr(p, 'z'): # No 3D pnt = OGRGeometry(p.wkt) self.assertEqual(1, pnt.geom_type) @@ -122,8 +127,7 @@ def test02_points(self): def test03_multipoints(self): "Testing MultiPoint objects." - - for mp in multipoints: + for mp in self.geometries.multipoints: mgeom1 = OGRGeometry(mp.wkt) # First one from WKT self.assertEqual(4, mgeom1.geom_type) self.assertEqual('MULTIPOINT', mgeom1.geom_name) @@ -134,38 +138,38 @@ def test03_multipoints(self): mgeom3.add(g.wkt) # should take WKT as well self.assertEqual(mgeom1, mgeom2) # they should equal self.assertEqual(mgeom1, mgeom3) - self.assertEqual(mp.points, mgeom2.tuple) + self.assertEqual(mp.coords, mgeom2.coords) self.assertEqual(mp.n_p, mgeom2.point_count) def test04_linestring(self): "Testing LineString objects." prev = OGRGeometry('POINT(0 0)') - for ls in linestrings: + for ls in self.geometries.linestrings: linestr = OGRGeometry(ls.wkt) self.assertEqual(2, linestr.geom_type) self.assertEqual('LINESTRING', linestr.geom_name) self.assertEqual(ls.n_p, linestr.point_count) - self.assertEqual(ls.tup, linestr.tuple) + self.assertEqual(ls.coords, linestr.tuple) self.assertEqual(True, linestr == OGRGeometry(ls.wkt)) self.assertEqual(True, linestr != prev) self.assertRaises(OGRIndexError, linestr.__getitem__, len(linestr)) prev = linestr # Testing the x, y properties. - x = [tmpx for tmpx, tmpy in ls.tup] - y = [tmpy for tmpx, tmpy in ls.tup] + x = [tmpx for tmpx, tmpy in ls.coords] + y = [tmpy for tmpx, tmpy in ls.coords] self.assertEqual(x, linestr.x) self.assertEqual(y, linestr.y) def test05_multilinestring(self): "Testing MultiLineString objects." prev = OGRGeometry('POINT(0 0)') - for mls in multilinestrings: + for mls in self.geometries.multilinestrings: mlinestr = OGRGeometry(mls.wkt) self.assertEqual(5, mlinestr.geom_type) self.assertEqual('MULTILINESTRING', mlinestr.geom_name) self.assertEqual(mls.n_p, mlinestr.point_count) - self.assertEqual(mls.tup, mlinestr.tuple) + self.assertEqual(mls.coords, mlinestr.tuple) self.assertEqual(True, mlinestr == OGRGeometry(mls.wkt)) self.assertEqual(True, mlinestr != prev) prev = mlinestr @@ -177,7 +181,7 @@ def test05_multilinestring(self): def test06_linearring(self): "Testing LinearRing objects." prev = OGRGeometry('POINT(0 0)') - for rr in linearrings: + for rr in self.geometries.linearrings: lr = OGRGeometry(rr.wkt) #self.assertEqual(101, lr.geom_type.num) self.assertEqual('LINEARRING', lr.geom_name) @@ -195,7 +199,7 @@ def test07a_polygons(self): self.assertEqual(bbox, p.extent) prev = OGRGeometry('POINT(0 0)') - for p in polygons: + for p in self.geometries.polygons: poly = OGRGeometry(p.wkt) self.assertEqual(3, poly.geom_type) self.assertEqual('POLYGON', poly.geom_name) @@ -238,10 +242,7 @@ def test07b_closepolygons(self): # Closing the rings -- doesn't work on GDAL versions 1.4.1 and below: # http://trac.osgeo.org/gdal/ticket/1673 - major, minor1, minor2 = gdal_version().split('.') - if major == '1': - iminor1 = int(minor1) - if iminor1 < 4 or (iminor1 == 4 and minor2.startswith('1')): return + if GDAL_VERSION <= (1, 4, 1): return poly.close_rings() self.assertEqual(10, poly.point_count) # Two closing points should've been added self.assertEqual(OGRGeometry('POINT(2.5 2.5)'), poly.centroid) @@ -249,7 +250,7 @@ def test07b_closepolygons(self): def test08_multipolygons(self): "Testing MultiPolygon objects." prev = OGRGeometry('POINT(0 0)') - for mp in multipolygons: + for mp in self.geometries.multipolygons: mpoly = OGRGeometry(mp.wkt) self.assertEqual(6, mpoly.geom_type) self.assertEqual('MULTIPOLYGON', mpoly.geom_name) @@ -264,7 +265,7 @@ def test08_multipolygons(self): def test09a_srs(self): "Testing OGR Geometries with Spatial Reference objects." - for mp in multipolygons: + for mp in self.geometries.multipolygons: # Creating a geometry w/spatial reference sr = SpatialReference('WGS84') mpoly = OGRGeometry(mp.wkt, sr) @@ -282,8 +283,8 @@ def test09a_srs(self): self.assertEqual(sr.wkt, ring.srs.wkt) # Ensuring SRS propagate in topological ops. - a, b = topology_geoms[0] - a, b = OGRGeometry(a.wkt, sr), OGRGeometry(b.wkt, sr) + a = OGRGeometry(self.geometries.topology_geoms[0].wkt_a, sr) + b = OGRGeometry(self.geometries.topology_geoms[0].wkt_b, sr) diff = a.difference(b) union = a.union(b) self.assertEqual(sr.wkt, diff.srs.wkt) @@ -351,11 +352,10 @@ def test09c_transform_dim(self): def test10_difference(self): "Testing difference()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = OGRGeometry(g_tup[0].wkt) - b = OGRGeometry(g_tup[1].wkt) - d1 = OGRGeometry(diff_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + d1 = OGRGeometry(self.geometries.diff_geoms[i].wkt) d2 = a.difference(b) self.assertEqual(d1, d2) self.assertEqual(d1, a - b) # __sub__ is difference operator @@ -364,11 +364,10 @@ def test10_difference(self): def test11_intersection(self): "Testing intersects() and intersection()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = OGRGeometry(g_tup[0].wkt) - b = OGRGeometry(g_tup[1].wkt) - i1 = OGRGeometry(intersect_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + i1 = OGRGeometry(self.geometries.intersect_geoms[i].wkt) self.assertEqual(True, a.intersects(b)) i2 = a.intersection(b) self.assertEqual(i1, i2) @@ -378,11 +377,10 @@ def test11_intersection(self): def test12_symdifference(self): "Testing sym_difference()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = OGRGeometry(g_tup[0].wkt) - b = OGRGeometry(g_tup[1].wkt) - d1 = OGRGeometry(sdiff_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + d1 = OGRGeometry(self.geometries.sdiff_geoms[i].wkt) d2 = a.sym_difference(b) self.assertEqual(d1, d2) self.assertEqual(d1, a ^ b) # __xor__ is symmetric difference operator @@ -391,11 +389,10 @@ def test12_symdifference(self): def test13_union(self): "Testing union()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = OGRGeometry(g_tup[0].wkt) - b = OGRGeometry(g_tup[1].wkt) - u1 = OGRGeometry(union_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = OGRGeometry(self.geometries.topology_geoms[i].wkt_a) + b = OGRGeometry(self.geometries.topology_geoms[i].wkt_b) + u1 = OGRGeometry(self.geometries.union_geoms[i].wkt) u2 = a.union(b) self.assertEqual(u1, u2) self.assertEqual(u1, a | b) # __or__ is union operator @@ -411,7 +408,7 @@ def test14_add(self): # GeometryCollection.add may take an OGRGeometry (if another collection # of the same type all child geoms will be added individually) or WKT. - for mp in multipolygons: + for mp in self.geometries.multipolygons: mpoly = OGRGeometry(mp.wkt) mp1 = OGRGeometry('MultiPolygon') mp2 = OGRGeometry('MultiPolygon') @@ -429,7 +426,7 @@ def test15_extent(self): mp = OGRGeometry('MULTIPOINT(5 23, 0 0, 10 50)') self.assertEqual((0.0, 0.0, 10.0, 50.0), mp.extent) # Testing on the 'real world' Polygon. - poly = OGRGeometry(polygons[3].wkt) + poly = OGRGeometry(self.geometries.polygons[3].wkt) ring = poly.shell x, y = ring.x, ring.y xmin, ymin = min(x), min(y) diff --git a/django/contrib/gis/gdal/tests/test_srs.py b/django/contrib/gis/gdal/tests/test_srs.py index 2742c7aad9f2..abec19a12de8 100644 --- a/django/contrib/gis/gdal/tests/test_srs.py +++ b/django/contrib/gis/gdal/tests/test_srs.py @@ -16,15 +16,13 @@ def __init__(self, wkt, **kwargs): attr=(('DATUM', 'WGS_1984'), (('SPHEROID', 1), '6378137'),('primem|authority', 'EPSG'),), ), TestSRS('PROJCS["NAD83 / Texas South Central",GEOGCS["NAD83",DATUM["North_American_Datum_1983",SPHEROID["GRS 1980",6378137,298.257222101,AUTHORITY["EPSG","7019"]],AUTHORITY["EPSG","6269"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4269"]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["standard_parallel_1",30.28333333333333],PARAMETER["standard_parallel_2",28.38333333333333],PARAMETER["latitude_of_origin",27.83333333333333],PARAMETER["central_meridian",-99],PARAMETER["false_easting",600000],PARAMETER["false_northing",4000000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AUTHORITY["EPSG","32140"]]', - proj='+proj=lcc +lat_1=30.28333333333333 +lat_2=28.38333333333333 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=4000000 +ellps=GRS80 +datum=NAD83 +units=m +no_defs ', - epsg=32140, projected=True, geographic=False, local=False, + proj=None, epsg=32140, projected=True, geographic=False, local=False, lin_name='metre', ang_name='degree', lin_units=1.0, ang_units=0.0174532925199, auth={'PROJCS' : ('EPSG', '32140'), 'spheroid' : ('EPSG', '7019'), 'unit' : ('EPSG', '9001'),}, attr=(('DATUM', 'North_American_Datum_1983'),(('SPHEROID', 2), '298.257222101'),('PROJECTION','Lambert_Conformal_Conic_2SP'),), ), TestSRS('PROJCS["NAD_1983_StatePlane_Texas_South_Central_FIPS_4204_Feet",GEOGCS["GCS_North_American_1983",DATUM["North_American_Datum_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Lambert_Conformal_Conic_2SP"],PARAMETER["False_Easting",1968500.0],PARAMETER["False_Northing",13123333.33333333],PARAMETER["Central_Meridian",-99.0],PARAMETER["Standard_Parallel_1",28.38333333333333],PARAMETER["Standard_Parallel_2",30.28333333333334],PARAMETER["Latitude_Of_Origin",27.83333333333333],UNIT["Foot_US",0.3048006096012192]]', - proj='+proj=lcc +lat_1=28.38333333333333 +lat_2=30.28333333333334 +lat_0=27.83333333333333 +lon_0=-99 +x_0=600000 +y_0=3999999.999999999 +ellps=GRS80 +datum=NAD83 +to_meter=0.3048006096012192 +no_defs ', - epsg=None, projected=True, geographic=False, local=False, + proj=None, epsg=None, projected=True, geographic=False, local=False, lin_name='Foot_US', ang_name='Degree', lin_units=0.3048006096012192, ang_units=0.0174532925199, auth={'PROJCS' : (None, None),}, attr=(('PROJCS|GeOgCs|spheroid', 'GRS_1980'),(('projcs', 9), 'UNIT'), (('projcs', 11), None),), @@ -73,7 +71,6 @@ def test03_get_wkt(self): def test04_proj(self): "Test PROJ.4 import and export." - for s in srlist: if s.proj: srs1 = SpatialReference(s.wkt) @@ -88,7 +85,6 @@ def test05_epsg(self): srs2 = SpatialReference(s.epsg) srs3 = SpatialReference(str(s.epsg)) srs4 = SpatialReference('EPSG:%d' % s.epsg) - #self.assertEqual(srs1.wkt, srs2.wkt) for srs in (srs1, srs2, srs3, srs4): for attr, expected in s.attr: self.assertEqual(expected, srs[attr]) @@ -157,7 +153,6 @@ def test13_attr_value(self): self.assertEqual('WGS_1984', s1['DATUM']) self.assertEqual('EPSG', s1['AUTHORITY']) self.assertEqual(4326, int(s1['AUTHORITY', 1])) - #for i in range(7): self.assertEqual(0, int(s1['TOWGS84', i])) self.assertEqual(None, s1['FOOBAR']) def suite(): diff --git a/django/contrib/gis/geometry/regex.py b/django/contrib/gis/geometry/regex.py index 85238581bf7c..1b9e2f46f427 100644 --- a/django/contrib/gis/geometry/regex.py +++ b/django/contrib/gis/geometry/regex.py @@ -2,7 +2,7 @@ # Regular expression for recognizing HEXEWKB and WKT. A prophylactic measure # to prevent potentially malicious input from reaching the underlying C -# library. Not a substitute for good web security programming practices. +# library. Not a substitute for good Web security programming practices. hex_regex = re.compile(r'^[0-9A-F]+$', re.I) wkt_regex = re.compile(r'^(SRID=(?P\d+);)?' r'(?P' diff --git a/django/contrib/gis/geometry/test_data.py b/django/contrib/gis/geometry/test_data.py new file mode 100644 index 000000000000..4e073487a5d7 --- /dev/null +++ b/django/contrib/gis/geometry/test_data.py @@ -0,0 +1,105 @@ +""" +This module has the mock object definitions used to hold reference geometry +for the GEOS and GDAL tests. +""" +import gzip +import os + +from django.contrib import gis +from django.utils import simplejson + + +# This global used to store reference geometry data. +GEOMETRIES = None + +# Path where reference test data is located. +TEST_DATA = os.path.join(os.path.dirname(gis.__file__), 'tests', 'data') + + +def tuplize(seq): + "Turn all nested sequences to tuples in given sequence." + if isinstance(seq, (list, tuple)): + return tuple([tuplize(i) for i in seq]) + return seq + + +def strconvert(d): + "Converts all keys in dictionary to str type." + return dict([(str(k), v) for k, v in d.iteritems()]) + + +def get_ds_file(name, ext): + return os.path.join(TEST_DATA, + name, + name + '.%s' % ext + ) + + +class TestObj(object): + """ + Base testing object, turns keyword args into attributes. + """ + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, value) + + +class TestDS(TestObj): + """ + Object for testing GDAL data sources. + """ + def __init__(self, name, **kwargs): + # Shapefile is default extension, unless specified otherwise. + ext = kwargs.pop('ext', 'shp') + self.ds = get_ds_file(name, ext) + super(TestDS, self).__init__(**kwargs) + + +class TestGeom(TestObj): + """ + Testing object used for wrapping reference geometry data + in GEOS/GDAL tests. + """ + def __init__(self, **kwargs): + # Converting lists to tuples of certain keyword args + # so coordinate test cases will match (JSON has no + # concept of tuple). + coords = kwargs.pop('coords', None) + if coords: + self.coords = tuplize(coords) + + centroid = kwargs.pop('centroid', None) + if centroid: + self.centroid = tuple(centroid) + + ext_ring_cs = kwargs.pop('ext_ring_cs', None) + if ext_ring_cs: + ext_ring_cs = tuplize(ext_ring_cs) + self.ext_ring_cs = ext_ring_cs + + super(TestGeom, self).__init__(**kwargs) + + +class TestGeomSet(object): + """ + Each attribute of this object is a list of `TestGeom` instances. + """ + def __init__(self, **kwargs): + for key, value in kwargs.items(): + setattr(self, key, [TestGeom(**strconvert(kw)) for kw in value]) + + +class TestDataMixin(object): + """ + Mixin used for GEOS/GDAL test cases that defines a `geometries` + property, which returns and/or loads the reference geometry data. + """ + @property + def geometries(self): + global GEOMETRIES + if GEOMETRIES is None: + # Load up the test geometry data from fixture into global. + gzf = gzip.GzipFile(os.path.join(TEST_DATA, 'geometries.json.gz')) + geometries = simplejson.loads(gzf.read()) + GEOMETRIES = TestGeomSet(**strconvert(geometries)) + return GEOMETRIES diff --git a/django/contrib/gis/geos/libgeos.py b/django/contrib/gis/geos/libgeos.py index 84299a05403f..7e1dfa0dc5ee 100644 --- a/django/contrib/gis/geos/libgeos.py +++ b/django/contrib/gis/geos/libgeos.py @@ -23,7 +23,7 @@ lib_names = None elif os.name == 'nt': # Windows NT libraries - lib_names = ['libgeos_c-1'] + lib_names = ['geos_c', 'libgeos_c-1'] elif os.name == 'posix': # *NIX libraries lib_names = ['geos_c', 'GEOS'] diff --git a/django/contrib/gis/geos/prototypes/threadsafe.py b/django/contrib/gis/geos/prototypes/threadsafe.py index 5888ed16e22b..2c9d25ee9f9b 100644 --- a/django/contrib/gis/geos/prototypes/threadsafe.py +++ b/django/contrib/gis/geos/prototypes/threadsafe.py @@ -20,18 +20,6 @@ class GEOSContext(threading.local): thread_context = GEOSContext() -def call_geos_threaded(cfunc, args): - """ - This module-level routine calls the specified GEOS C thread-safe - function with the context for this current thread. - """ - # If a context handle does not exist for this thread, initialize one. - if not thread_context.handle: - thread_context.handle = GEOSContextHandle() - # Call the threaded GEOS routine with pointer of the context handle - # as the first argument. - return cfunc(thread_context.handle.ptr, *args) - class GEOSFunc(object): """ Class that serves as a wrapper for GEOS C Functions, and will @@ -43,6 +31,9 @@ def __init__(self, func_name): # take an additional context handle parameter. self.cfunc = getattr(lgeos, func_name + '_r') self.threaded = True + # Create a reference here to thread_context so it's not + # garbage-collected before an attempt to call this object. + self.thread_context = thread_context except AttributeError: # Otherwise, use usual function. self.cfunc = getattr(lgeos, func_name) @@ -50,7 +41,12 @@ def __init__(self, func_name): def __call__(self, *args): if self.threaded: - return call_geos_threaded(self.cfunc, args) + # If a context handle does not exist for this thread, initialize one. + if not self.thread_context.handle: + self.thread_context.handle = GEOSContextHandle() + # Call the threaded GEOS routine with pointer of the context handle + # as the first argument. + return self.cfunc(self.thread_context.handle.ptr, *args) else: return self.cfunc(*args) diff --git a/django/contrib/gis/geos/tests/test_geos.py b/django/contrib/gis/geos/tests/test_geos.py index 4f2e33f9a034..3cd021e8b826 100644 --- a/django/contrib/gis/geos/tests/test_geos.py +++ b/django/contrib/gis/geos/tests/test_geos.py @@ -1,9 +1,9 @@ import ctypes, random, unittest, sys from django.contrib.gis.geos import * from django.contrib.gis.geos.base import gdal, numpy, GEOSBase -from django.contrib.gis.tests.geometries import * +from django.contrib.gis.geometry.test_data import TestDataMixin -class GEOSTest(unittest.TestCase): +class GEOSTest(unittest.TestCase, TestDataMixin): @property def null_srid(self): @@ -61,13 +61,13 @@ class FakeGeom2(GEOSBase): def test01a_wkt(self): "Testing WKT output." - for g in wkt_out: + for g in self.geometries.wkt_out: geom = fromstr(g.wkt) self.assertEqual(g.ewkt, geom.wkt) def test01b_hex(self): "Testing HEX output." - for g in hex_wkt: + for g in self.geometries.hex_wkt: geom = fromstr(g.wkt) self.assertEqual(g.hex, geom.hex) @@ -75,9 +75,16 @@ def test01b_hexewkb(self): "Testing (HEX)EWKB output." from binascii import a2b_hex + # For testing HEX(EWKB). + ogc_hex = '01010000000000000000000000000000000000F03F' + # `SELECT ST_AsHEXEWKB(ST_GeomFromText('POINT(0 1)', 4326));` + hexewkb_2d = '0101000020E61000000000000000000000000000000000F03F' + # `SELECT ST_AsHEXEWKB(ST_GeomFromEWKT('SRID=4326;POINT(0 1 2)'));` + hexewkb_3d = '01010000A0E61000000000000000000000000000000000F03F0000000000000040' + pnt_2d = Point(0, 1, srid=4326) pnt_3d = Point(0, 1, 2, srid=4326) - + # OGC-compliant HEX will not have SRID nor Z value. self.assertEqual(ogc_hex, pnt_2d.hex) self.assertEqual(ogc_hex, pnt_3d.hex) @@ -110,13 +117,13 @@ def test01b_hexewkb(self): pass else: self.fail('Should have raised GEOSException') - + # Redundant sanity check. self.assertEqual(4326, GEOSGeometry(hexewkb_2d).srid) def test01c_kml(self): "Testing KML output." - for tg in wkt_out: + for tg in self.geometries.wkt_out: geom = fromstr(tg.wkt) kml = getattr(tg, 'kml', False) if kml: self.assertEqual(kml, geom.kml) @@ -125,7 +132,7 @@ def test01d_errors(self): "Testing the Error handlers." # string-based print "\nBEGIN - expecting GEOS_ERROR; safe to ignore.\n" - for err in errors: + for err in self.geometries.errors: try: g = fromstr(err.wkt) except (GEOSException, ValueError): @@ -147,14 +154,14 @@ class NotAGeometry(object): def test01e_wkb(self): "Testing WKB output." from binascii import b2a_hex - for g in hex_wkt: + for g in self.geometries.hex_wkt: geom = fromstr(g.wkt) wkb = geom.wkb self.assertEqual(b2a_hex(wkb).upper(), g.hex) def test01f_create_hex(self): "Testing creation from HEX." - for g in hex_wkt: + for g in self.geometries.hex_wkt: geom_h = GEOSGeometry(g.hex) # we need to do this so decimal places get normalised geom_t = fromstr(g.wkt) @@ -163,7 +170,7 @@ def test01f_create_hex(self): def test01g_create_wkb(self): "Testing creation from WKB." from binascii import a2b_hex - for g in hex_wkt: + for g in self.geometries.hex_wkt: wkb = buffer(a2b_hex(g.hex)) geom_h = GEOSGeometry(wkb) # we need to do this so decimal places get normalised @@ -173,7 +180,7 @@ def test01g_create_wkb(self): def test01h_ewkt(self): "Testing EWKT." srid = 32140 - for p in polygons: + for p in self.geometries.polygons: ewkt = 'SRID=%d;%s' % (srid, p.wkt) poly = fromstr(ewkt) self.assertEqual(srid, poly.srid) @@ -183,7 +190,7 @@ def test01h_ewkt(self): def test01i_json(self): "Testing GeoJSON input/output (via GDAL)." if not gdal or not gdal.GEOJSON: return - for g in json_geoms: + for g in self.geometries.json_geoms: geom = GEOSGeometry(g.wkt) if not hasattr(g, 'not_equal'): self.assertEqual(g.json, geom.json) @@ -225,7 +232,7 @@ def test01k_eq(self): def test02a_points(self): "Testing Point objects." prev = fromstr('POINT(0 0)') - for p in points: + for p in self.geometries.points: # Creating the point from the WKT pnt = fromstr(p.wkt) self.assertEqual(pnt.geom_type, 'Point') @@ -279,7 +286,7 @@ def test02a_points(self): def test02b_multipoints(self): "Testing MultiPoint objects." - for mp in multipoints: + for mp in self.geometries.multipoints: mpnt = fromstr(mp.wkt) self.assertEqual(mpnt.geom_type, 'MultiPoint') self.assertEqual(mpnt.geom_typeid, 4) @@ -289,7 +296,7 @@ def test02b_multipoints(self): self.assertRaises(GEOSIndexError, mpnt.__getitem__, len(mpnt)) self.assertEqual(mp.centroid, mpnt.centroid.tuple) - self.assertEqual(mp.points, tuple(m.tuple for m in mpnt)) + self.assertEqual(mp.coords, tuple(m.tuple for m in mpnt)) for p in mpnt: self.assertEqual(p.geom_type, 'Point') self.assertEqual(p.geom_typeid, 0) @@ -299,7 +306,7 @@ def test02b_multipoints(self): def test03a_linestring(self): "Testing LineString objects." prev = fromstr('POINT(0 0)') - for l in linestrings: + for l in self.geometries.linestrings: ls = fromstr(l.wkt) self.assertEqual(ls.geom_type, 'LineString') self.assertEqual(ls.geom_typeid, 1) @@ -325,7 +332,7 @@ def test03a_linestring(self): def test03b_multilinestring(self): "Testing MultiLineString objects." prev = fromstr('POINT(0 0)') - for l in multilinestrings: + for l in self.geometries.multilinestrings: ml = fromstr(l.wkt) self.assertEqual(ml.geom_type, 'MultiLineString') self.assertEqual(ml.geom_typeid, 5) @@ -348,7 +355,7 @@ def test03b_multilinestring(self): def test04_linearring(self): "Testing LinearRing objects." - for rr in linearrings: + for rr in self.geometries.linearrings: lr = fromstr(rr.wkt) self.assertEqual(lr.geom_type, 'LinearRing') self.assertEqual(lr.geom_typeid, 2) @@ -371,7 +378,7 @@ def test05a_polygons(self): self.assertEqual(bbox, p.extent) prev = fromstr('POINT(0 0)') - for p in polygons: + for p in self.geometries.polygons: # Creating the Polygon, testing its properties. poly = fromstr(p.wkt) self.assertEqual(poly.geom_type, 'Polygon') @@ -430,7 +437,7 @@ def test05b_multipolygons(self): "Testing MultiPolygon objects." print "\nBEGIN - expecting GEOS_NOTICE; safe to ignore.\n" prev = fromstr('POINT (0 0)') - for mp in multipolygons: + for mp in self.geometries.multipolygons: mpoly = fromstr(mp.wkt) self.assertEqual(mpoly.geom_type, 'MultiPolygon') self.assertEqual(mpoly.geom_typeid, 6) @@ -456,7 +463,7 @@ def test06a_memory_hijinks(self): # These tests are needed to ensure sanity with writable geometries. # Getting a polygon with interior rings, and pulling out the interior rings - poly = fromstr(polygons[1].wkt) + poly = fromstr(self.geometries.polygons[1].wkt) ring1 = poly[0] ring2 = poly[1] @@ -472,12 +479,9 @@ def test06a_memory_hijinks(self): # Access to these rings is OK since they are clones. s1, s2 = str(ring1), str(ring2) - # The previous hijinks tests are now moot because only clones are - # now used =) - def test08_coord_seq(self): "Testing Coordinate Sequence objects." - for p in polygons: + for p in self.geometries.polygons: if p.ext_ring_cs: # Constructing the polygon and getting the coordinate sequence poly = fromstr(p.wkt) @@ -506,22 +510,18 @@ def test09_relate_pattern(self): "Testing relate() and relate_pattern()." g = fromstr('POINT (0 0)') self.assertRaises(GEOSException, g.relate_pattern, 0, 'invalid pattern, yo') - for i in xrange(len(relate_geoms)): - g_tup = relate_geoms[i] - a = fromstr(g_tup[0].wkt) - b = fromstr(g_tup[1].wkt) - pat = g_tup[2] - result = g_tup[3] - self.assertEqual(result, a.relate_pattern(b, pat)) - self.assertEqual(pat, a.relate(b)) + for rg in self.geometries.relate_geoms: + a = fromstr(rg.wkt_a) + b = fromstr(rg.wkt_b) + self.assertEqual(rg.result, a.relate_pattern(b, rg.pattern)) + self.assertEqual(rg.pattern, a.relate(b)) def test10_intersection(self): "Testing intersects() and intersection()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = fromstr(g_tup[0].wkt) - b = fromstr(g_tup[1].wkt) - i1 = fromstr(intersect_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + i1 = fromstr(self.geometries.intersect_geoms[i].wkt) self.assertEqual(True, a.intersects(b)) i2 = a.intersection(b) self.assertEqual(i1, i2) @@ -531,11 +531,10 @@ def test10_intersection(self): def test11_union(self): "Testing union()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = fromstr(g_tup[0].wkt) - b = fromstr(g_tup[1].wkt) - u1 = fromstr(union_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + u1 = fromstr(self.geometries.union_geoms[i].wkt) u2 = a.union(b) self.assertEqual(u1, u2) self.assertEqual(u1, a | b) # __or__ is union operator @@ -544,11 +543,10 @@ def test11_union(self): def test12_difference(self): "Testing difference()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = fromstr(g_tup[0].wkt) - b = fromstr(g_tup[1].wkt) - d1 = fromstr(diff_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + d1 = fromstr(self.geometries.diff_geoms[i].wkt) d2 = a.difference(b) self.assertEqual(d1, d2) self.assertEqual(d1, a - b) # __sub__ is difference operator @@ -557,11 +555,10 @@ def test12_difference(self): def test13_symdifference(self): "Testing sym_difference()." - for i in xrange(len(topology_geoms)): - g_tup = topology_geoms[i] - a = fromstr(g_tup[0].wkt) - b = fromstr(g_tup[1].wkt) - d1 = fromstr(sdiff_geoms[i].wkt) + for i in xrange(len(self.geometries.topology_geoms)): + a = fromstr(self.geometries.topology_geoms[i].wkt_a) + b = fromstr(self.geometries.topology_geoms[i].wkt_b) + d1 = fromstr(self.geometries.sdiff_geoms[i].wkt) d2 = a.sym_difference(b) self.assertEqual(d1, d2) self.assertEqual(d1, a ^ b) # __xor__ is symmetric difference operator @@ -570,18 +567,19 @@ def test13_symdifference(self): def test14_buffer(self): "Testing buffer()." - for i in xrange(len(buffer_geoms)): - g_tup = buffer_geoms[i] - g = fromstr(g_tup[0].wkt) + for bg in self.geometries.buffer_geoms: + g = fromstr(bg.wkt) # The buffer we expect - exp_buf = fromstr(g_tup[1].wkt) + exp_buf = fromstr(bg.buffer_wkt) + quadsegs = bg.quadsegs + width = bg.width # Can't use a floating-point for the number of quadsegs. - self.assertRaises(ctypes.ArgumentError, g.buffer, g_tup[2], float(g_tup[3])) + self.assertRaises(ctypes.ArgumentError, g.buffer, width, float(quadsegs)) # Constructing our buffer - buf = g.buffer(g_tup[2], g_tup[3]) + buf = g.buffer(width, quadsegs) self.assertEqual(exp_buf.num_coords, buf.num_coords) self.assertEqual(len(exp_buf), len(buf)) @@ -605,7 +603,7 @@ def test15_srid(self): self.assertRaises(ctypes.ArgumentError, pnt.set_srid, '4326') # Testing SRID keyword on fromstr(), and on Polygon rings. - poly = fromstr(polygons[1].wkt, srid=4269) + poly = fromstr(self.geometries.polygons[1].wkt, srid=4269) self.assertEqual(4269, poly.srid) for ring in poly: self.assertEqual(4269, ring.srid) poly.srid = 4326 @@ -636,7 +634,7 @@ def test15_srid(self): def test16_mutable_geometries(self): "Testing the mutability of Polygons and Geometry Collections." ### Testing the mutability of Polygons ### - for p in polygons: + for p in self.geometries.polygons: poly = fromstr(p.wkt) # Should only be able to use __setitem__ with LinearRing geometries. @@ -655,7 +653,7 @@ def test16_mutable_geometries(self): self.assertEqual(poly[0], new_shell) ### Testing the mutability of Geometry Collections - for tg in multipoints: + for tg in self.geometries.multipoints: mp = fromstr(tg.wkt) for i in range(len(mp)): # Creating a random point. @@ -670,7 +668,7 @@ def test16_mutable_geometries(self): # MultiPolygons involve much more memory management because each # Polygon w/in the collection has its own rings. - for tg in multipolygons: + for tg in self.geometries.multipolygons: mpoly = fromstr(tg.wkt) for i in xrange(len(mpoly)): poly = mpoly[i] @@ -791,10 +789,10 @@ def test20b_collections_of_collections(self): "Testing GeometryCollection handling of other collections." # Creating a GeometryCollection WKT string composed of other # collections and polygons. - coll = [mp.wkt for mp in multipolygons if mp.valid] - coll.extend([mls.wkt for mls in multilinestrings]) - coll.extend([p.wkt for p in polygons]) - coll.extend([mp.wkt for mp in multipoints]) + coll = [mp.wkt for mp in self.geometries.multipolygons if mp.valid] + coll.extend([mls.wkt for mls in self.geometries.multilinestrings]) + coll.extend([p.wkt for p in self.geometries.polygons]) + coll.extend([mp.wkt for mp in self.geometries.multipoints]) gc_wkt = 'GEOMETRYCOLLECTION(%s)' % ','.join(coll) # Should construct ok from WKT @@ -862,7 +860,7 @@ def test24_extent(self): # Extent of points is just the point itself repeated. self.assertEqual((5.23, 17.8, 5.23, 17.8), pnt.extent) # Testing on the 'real world' Polygon. - poly = fromstr(polygons[3].wkt) + poly = fromstr(self.geometries.polygons[3].wkt) ring = poly.shell x, y = ring.x, ring.y xmin, ymin = min(x), min(y) @@ -878,10 +876,10 @@ def test25_pickle(self): # and setting the SRID on some of them. def get_geoms(lst, srid=None): return [GEOSGeometry(tg.wkt, srid) for tg in lst] - tgeoms = get_geoms(points) - tgeoms.extend(get_geoms(multilinestrings, 4326)) - tgeoms.extend(get_geoms(polygons, 3084)) - tgeoms.extend(get_geoms(multipolygons, 900913)) + tgeoms = get_geoms(self.geometries.points) + tgeoms.extend(get_geoms(self.geometries.multilinestrings, 4326)) + tgeoms.extend(get_geoms(self.geometries.polygons, 3084)) + tgeoms.extend(get_geoms(self.geometries.multipolygons, 900913)) # The SRID won't be exported in GEOS 3.0 release candidates. no_srid = self.null_srid == -1 diff --git a/django/contrib/gis/maps/google/__init__.py b/django/contrib/gis/maps/google/__init__.py index e1e38a9aff82..9be689c07abf 100644 --- a/django/contrib/gis/maps/google/__init__.py +++ b/django/contrib/gis/maps/google/__init__.py @@ -1,6 +1,6 @@ """ This module houses the GoogleMap object, used for generating - the needed javascript to embed Google Maps in a webpage. + the needed javascript to embed Google Maps in a Web page. Google(R) is a registered trademark of Google, Inc. of Mountain View, California. diff --git a/django/contrib/gis/sitemaps/georss.py b/django/contrib/gis/sitemaps/georss.py index aca53a43c963..f75cf804ba54 100644 --- a/django/contrib/gis/sitemaps/georss.py +++ b/django/contrib/gis/sitemaps/georss.py @@ -36,12 +36,12 @@ def __init__(self, feed_dict, slug_dict=None): else: self.locations.append(section) - def get_urls(self, page=1): + def get_urls(self, page=1, site=None): """ This method is overrridden so the appropriate `geo_format` attribute is placed on each URL element. """ - urls = Sitemap.get_urls(self, page=page) + urls = Sitemap.get_urls(self, page=page, site=site) for url in urls: url['geo_format'] = 'georss' return urls diff --git a/django/contrib/gis/sitemaps/kml.py b/django/contrib/gis/sitemaps/kml.py index d85744f0f918..db30606b04e8 100644 --- a/django/contrib/gis/sitemaps/kml.py +++ b/django/contrib/gis/sitemaps/kml.py @@ -40,12 +40,12 @@ def _build_kml_sources(self, sources): raise TypeError('KML Sources must be a model or a 3-tuple.') return kml_sources - def get_urls(self, page=1): + def get_urls(self, page=1, site=None): """ This method is overrridden so the appropriate `geo_format` attribute is placed on each URL element. """ - urls = Sitemap.get_urls(self, page=page) + urls = Sitemap.get_urls(self, page=page, site=site) for url in urls: url['geo_format'] = self.geo_format return urls diff --git a/django/contrib/gis/sitemaps/views.py b/django/contrib/gis/sitemaps/views.py index f3c1da24ed8c..02a0fc02ab6e 100644 --- a/django/contrib/gis/sitemaps/views.py +++ b/django/contrib/gis/sitemaps/views.py @@ -1,6 +1,6 @@ from django.http import HttpResponse, Http404 from django.template import loader -from django.contrib.sites.models import Site +from django.contrib.sites.models import get_current_site from django.core import urlresolvers from django.core.paginator import EmptyPage, PageNotAnInteger from django.contrib.gis.db.models.fields import GeometryField @@ -15,7 +15,7 @@ def index(request, sitemaps): This view generates a sitemap index that uses the proper view for resolving geographic section sitemap URLs. """ - current_site = Site.objects.get_current() + current_site = get_current_site(request) sites = [] protocol = request.is_secure() and 'https' or 'http' for section, site in sitemaps.items(): @@ -46,12 +46,13 @@ def sitemap(request, sitemaps, section=None): maps = sitemaps.values() page = request.GET.get("p", 1) + current_site = get_current_site(request) for site in maps: try: if callable(site): - urls.extend(site().get_urls(page)) + urls.extend(site().get_urls(page=page, site=current_site)) else: - urls.extend(site.get_urls(page)) + urls.extend(site.get_urls(page=page, site=current_site)) except EmptyPage: raise Http404("Page %s empty" % page) except PageNotAnInteger: diff --git a/django/contrib/gis/templates/gis/admin/openlayers.js b/django/contrib/gis/templates/gis/admin/openlayers.js index 4324693c7822..c455bffd58dd 100644 --- a/django/contrib/gis/templates/gis/admin/openlayers.js +++ b/django/contrib/gis/templates/gis/admin/openlayers.js @@ -1,4 +1,5 @@ {# Author: Justin Bronn, Travis Pinney & Dane Springmeyer #} +OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.SphericalMercator.projectForward); {% block vars %}var {{ module }} = {}; {{ module }}.map = null; {{ module }}.controls = null; {{ module }}.panel = null; {{ module }}.re = new RegExp("^SRID=\d+;(.+)", "i"); {{ module }}.layers = {}; {{ module }}.modifiable = {{ modifiable|yesno:"true,false" }}; diff --git a/django/contrib/gis/tests/__init__.py b/django/contrib/gis/tests/__init__.py index 0d862190c0eb..138c291de93e 100644 --- a/django/contrib/gis/tests/__init__.py +++ b/django/contrib/gis/tests/__init__.py @@ -1,115 +1,141 @@ -import sys +import unittest + +from django.conf import settings +from django.test.simple import build_suite, DjangoTestSuiteRunner + def run_tests(*args, **kwargs): from django.test.simple import run_tests as base_run_tests return base_run_tests(*args, **kwargs) -def geo_suite(): + +def run_gis_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None): + import warnings + warnings.warn( + 'The run_gis_tests() test runner has been deprecated in favor of GeoDjangoTestSuiteRunner.', + PendingDeprecationWarning + ) + test_runner = GeoDjangoTestSuiteRunner(verbosity=verbosity, interactive=interactive, failfast=failfast) + return test_runner.run_tests(test_labels, extra_tests=extra_tests) + + +def geo_apps(namespace=True, runtests=False): """ - Builds a test suite for the GIS package. This is not named - `suite` so it will not interfere with the Django test suite (since - spatial database tables are required to execute these tests on - some backends). + Returns a list of GeoDjango test applications that reside in + `django.contrib.gis.tests` that can be used with the current + database and the spatial libraries that are installed. """ - from django.conf import settings + from django.db import connection from django.contrib.gis.geos import GEOS_PREPARE from django.contrib.gis.gdal import HAS_GDAL - from django.contrib.gis.utils import HAS_GEOIP - from django.contrib.gis.tests.utils import postgis, mysql - from django.db import connection - from django.utils.importlib import import_module - gis_tests = [] + apps = ['geoapp', 'relatedapp'] + + # No distance queries on MySQL. + if not connection.ops.mysql: + apps.append('distapp') + + # Test geography support with PostGIS 1.5+. + if connection.ops.postgis and connection.ops.geography: + apps.append('geogapp') + + # The following GeoDjango test apps depend on GDAL support. + if HAS_GDAL: + # 3D apps use LayerMapping, which uses GDAL. + if connection.ops.postgis and GEOS_PREPARE: + apps.append('geo3d') + + apps.append('layermap') + + if runtests: + return [('django.contrib.gis.tests', app) for app in apps] + elif namespace: + return ['django.contrib.gis.tests.%s' % app + for app in apps] + else: + return apps + + +def geodjango_suite(apps=True): + """ + Returns a TestSuite consisting only of GeoDjango tests that can be run. + """ + import sys + from django.db.models import get_app + + suite = unittest.TestSuite() # Adding the GEOS tests. from django.contrib.gis.geos import tests as geos_tests - gis_tests.append(geos_tests.suite()) - - # Tests that require use of a spatial database (e.g., creation of models) - test_apps = ['geoapp', 'relatedapp'] - if postgis and connection.ops.geography: - # Test geography support with PostGIS 1.5+. - test_apps.append('geogapp') + suite.addTest(geos_tests.suite()) - # Tests that do not require setting up and tearing down a spatial database. - test_suite_names = [ - 'test_measure', - ] + # Adding the measurment tests. + from django.contrib.gis.tests import test_measure + suite.addTest(test_measure.suite()) + # Adding GDAL tests, and any test suite that depends on GDAL, to the + # suite if GDAL is available. + from django.contrib.gis.gdal import HAS_GDAL if HAS_GDAL: - # These tests require GDAL. - if not mysql: - test_apps.append('distapp') - - # Only PostGIS using GEOS 3.1+ can support 3D so far. - if postgis and GEOS_PREPARE: - test_apps.append('geo3d') - - test_suite_names.extend(['test_spatialrefsys', 'test_geoforms']) - test_apps.append('layermap') - - # Adding the GDAL tests. from django.contrib.gis.gdal import tests as gdal_tests - gis_tests.append(gdal_tests.suite()) + suite.addTest(gdal_tests.suite()) + + from django.contrib.gis.tests import test_spatialrefsys, test_geoforms + suite.addTest(test_spatialrefsys.suite()) + suite.addTest(test_geoforms.suite()) else: - print >>sys.stderr, "GDAL not available - no tests requiring GDAL will be run." + sys.stderr.write('GDAL not available - no tests requiring GDAL will be run.\n') + # Add GeoIP tests to the suite, if the library and data is available. + from django.contrib.gis.utils import HAS_GEOIP if HAS_GEOIP and hasattr(settings, 'GEOIP_PATH'): - test_suite_names.append('test_geoip') + from django.contrib.gis.tests import test_geoip + suite.addTest(test_geoip.suite()) - # Adding the rest of the suites from the modules specified - # in the `test_suite_names`. - for suite_name in test_suite_names: - tsuite = import_module('django.contrib.gis.tests.' + suite_name) - gis_tests.append(tsuite.suite()) + # Finally, adding the suites for each of the GeoDjango test apps. + if apps: + for app_name in geo_apps(namespace=False): + suite.addTest(build_suite(get_app(app_name))) - return gis_tests, test_apps + return suite -def run_gis_tests(test_labels, **kwargs): - """ - Use this routine as the TEST_RUNNER in your settings in order to run the - GeoDjango test suite. This must be done as a database superuser for - PostGIS, so read the docstring in `run_test()` below for more details. - """ - from django.conf import settings - from django.db.models import loading - from django.contrib.gis.tests.utils import mysql - - # Getting initial values. - old_installed = settings.INSTALLED_APPS - old_root_urlconf = settings.ROOT_URLCONF - - # Overridding the INSTALLED_APPS with only what we need, - # to prevent unnecessary database table creation. - new_installed = ['django.contrib.sites', - 'django.contrib.sitemaps', - 'django.contrib.gis', - ] - - # Setting the URLs. - settings.ROOT_URLCONF = 'django.contrib.gis.tests.urls' - - # Creating the test suite, adding the test models to INSTALLED_APPS - # so they will be tested. - gis_tests, test_apps = geo_suite() - for test_model in test_apps: - module_name = 'django.contrib.gis.tests.%s' % test_model - new_installed.append(module_name) - - # Resetting the loaded flag to take into account what we appended to - # the INSTALLED_APPS (since this routine is invoked through - # django/core/management, it caches the apps; this ensures that syncdb - # will see our appended models) - settings.INSTALLED_APPS = new_installed - loading.cache.loaded = False - - kwargs['extra_tests'] = gis_tests - - # Running the tests using the GIS test runner. - result = run_tests(test_labels, **kwargs) - - # Restoring modified settings. - settings.INSTALLED_APPS = old_installed - settings.ROOT_URLCONF = old_root_urlconf - - return result + +class GeoDjangoTestSuiteRunner(DjangoTestSuiteRunner): + + def setup_test_environment(self, **kwargs): + super(GeoDjangoTestSuiteRunner, self).setup_test_environment(**kwargs) + + # Saving original values of INSTALLED_APPS, ROOT_URLCONF, and SITE_ID. + self.old_installed = getattr(settings, 'INSTALLED_APPS', None) + self.old_root_urlconf = getattr(settings, 'ROOT_URLCONF', '') + self.old_site_id = getattr(settings, 'SITE_ID', None) + + # Constructing the new INSTALLED_APPS, and including applications + # within the GeoDjango test namespace. + new_installed = ['django.contrib.sites', + 'django.contrib.sitemaps', + 'django.contrib.gis', + ] + + # Calling out to `geo_apps` to get GeoDjango applications supported + # for testing. + new_installed.extend(geo_apps()) + settings.INSTALLED_APPS = new_installed + + # SITE_ID needs to be set + settings.SITE_ID = 1 + + # ROOT_URLCONF needs to be set, else `AttributeErrors` are raised + # when TestCases are torn down that have `urls` defined. + settings.ROOT_URLCONF = '' + + + def teardown_test_environment(self, **kwargs): + super(GeoDjangoTestSuiteRunner, self).teardown_test_environment(**kwargs) + settings.INSTALLED_APPS = self.old_installed + settings.ROOT_URLCONF = self.old_root_urlconf + settings.SITE_ID = self.old_site_id + + + def build_suite(self, test_labels, extra_tests=None, **kwargs): + return geodjango_suite() diff --git a/django/contrib/gis/tests/data/geometries.json.gz b/django/contrib/gis/tests/data/geometries.json.gz new file mode 100644 index 000000000000..683dc83e4d7b Binary files /dev/null and b/django/contrib/gis/tests/data/geometries.json.gz differ diff --git a/django/contrib/gis/tests/data/invalid/emptypoints.dbf b/django/contrib/gis/tests/data/invalid/emptypoints.dbf new file mode 100644 index 000000000000..aa2109047a4f Binary files /dev/null and b/django/contrib/gis/tests/data/invalid/emptypoints.dbf differ diff --git a/django/contrib/gis/tests/data/invalid/emptypoints.shp b/django/contrib/gis/tests/data/invalid/emptypoints.shp new file mode 100644 index 000000000000..bdcfb83be42f Binary files /dev/null and b/django/contrib/gis/tests/data/invalid/emptypoints.shp differ diff --git a/django/contrib/gis/tests/data/invalid/emptypoints.shx b/django/contrib/gis/tests/data/invalid/emptypoints.shx new file mode 100644 index 000000000000..dea663eb087d Binary files /dev/null and b/django/contrib/gis/tests/data/invalid/emptypoints.shx differ diff --git a/django/contrib/gis/tests/distapp/data.py b/django/contrib/gis/tests/distapp/data.py deleted file mode 100644 index 77c06d65d867..000000000000 --- a/django/contrib/gis/tests/distapp/data.py +++ /dev/null @@ -1,36 +0,0 @@ -au_cities = (('Wollongong', 150.902, -34.4245), - ('Shellharbour', 150.87, -34.5789), - ('Thirroul', 150.924, -34.3147), - ('Mittagong', 150.449, -34.4509), - ('Batemans Bay', 150.175, -35.7082), - ('Canberra', 144.963, -37.8143), - ('Melbourne', 145.963, -37.8143), - ('Sydney', 151.26071, -33.887034), - ('Hobart', 147.33, -42.8827), - ('Adelaide', 138.6, -34.9258), - ('Hillsdale', 151.231341, -33.952685), - ) - -stx_cities = (('Downtown Houston', -95.363151, 29.763374), - ('West University Place', -95.448601, 29.713803), - ('Southside Place', -95.436920, 29.705777), - ('Bellaire', -95.458732, 29.705614), - ('Pearland', -95.287303, 29.563568), - ('Galveston', -94.797489, 29.301336), - ('Sealy', -96.156952, 29.780918), - ('San Antonio', -98.493183, 29.424170), - ('Saint Hedwig', -98.199820, 29.414197), - ) - -# Data from U.S. Census ZCTA cartographic boundary file for Texas (`zt48_d00.shp`). -stx_zips = (('77002', 'POLYGON ((-95.365015 29.772327, -95.362415 29.772327, -95.360915 29.771827, -95.354615 29.771827, -95.351515 29.772527, -95.350915 29.765327, -95.351015 29.762436, -95.350115 29.760328, -95.347515 29.758528, -95.352315 29.753928, -95.356415 29.756328, -95.358215 29.754028, -95.360215 29.756328, -95.363415 29.757128, -95.364014 29.75638, -95.363415 29.753928, -95.360015 29.751828, -95.361815 29.749528, -95.362715 29.750028, -95.367516 29.744128, -95.369316 29.745128, -95.373916 29.744128, -95.380116 29.738028, -95.387916 29.727929, -95.388516 29.729629, -95.387916 29.732129, -95.382916 29.737428, -95.376616 29.742228, -95.372616 29.747228, -95.378601 29.750846, -95.378616 29.752028, -95.378616 29.754428, -95.376016 29.754528, -95.374616 29.759828, -95.373616 29.761128, -95.371916 29.763928, -95.372316 29.768727, -95.365884 29.76791, -95.366015 29.767127, -95.358715 29.765327, -95.358615 29.766327, -95.359115 29.767227, -95.360215 29.767027, -95.362783 29.768267, -95.365315 29.770527, -95.365015 29.772327))'), - ('77005', 'POLYGON ((-95.447918 29.727275, -95.428017 29.728729, -95.421117 29.729029, -95.418617 29.727629, -95.418517 29.726429, -95.402117 29.726629, -95.402117 29.725729, -95.395316 29.725729, -95.391916 29.726229, -95.389716 29.725829, -95.396517 29.715429, -95.397517 29.715929, -95.400917 29.711429, -95.411417 29.715029, -95.418417 29.714729, -95.418317 29.70623, -95.440818 29.70593, -95.445018 29.70683, -95.446618 29.70763, -95.447418 29.71003, -95.447918 29.727275))'), - ('77025', 'POLYGON ((-95.418317 29.70623, -95.414717 29.706129, -95.414617 29.70533, -95.418217 29.70533, -95.419817 29.69533, -95.419484 29.694196, -95.417166 29.690901, -95.414517 29.69433, -95.413317 29.69263, -95.412617 29.68973, -95.412817 29.68753, -95.414087 29.685055, -95.419165 29.685428, -95.421617 29.68513, -95.425717 29.67983, -95.425017 29.67923, -95.424517 29.67763, -95.427418 29.67763, -95.438018 29.664631, -95.436713 29.664411, -95.440118 29.662231, -95.439218 29.661031, -95.437718 29.660131, -95.435718 29.659731, -95.431818 29.660331, -95.441418 29.656631, -95.441318 29.656331, -95.441818 29.656131, -95.441718 29.659031, -95.441118 29.661031, -95.446718 29.656431, -95.446518 29.673431, -95.446918 29.69013, -95.447418 29.71003, -95.446618 29.70763, -95.445018 29.70683, -95.440818 29.70593, -95.418317 29.70623))'), - ('77401', 'POLYGON ((-95.447918 29.727275, -95.447418 29.71003, -95.446918 29.69013, -95.454318 29.68893, -95.475819 29.68903, -95.475819 29.69113, -95.484419 29.69103, -95.484519 29.69903, -95.480419 29.70133, -95.480419 29.69833, -95.474119 29.69833, -95.474119 29.70453, -95.472719 29.71283, -95.468019 29.71293, -95.468219 29.720229, -95.464018 29.720229, -95.464118 29.724529, -95.463018 29.725929, -95.459818 29.726129, -95.459918 29.720329, -95.451418 29.720429, -95.451775 29.726303, -95.451318 29.727029, -95.447918 29.727275))'), - ) - -interstates = (('I-25', 'LINESTRING(-104.4780170766108 36.66698791870694, -104.4468522338495 36.79925409393386, -104.46212692626 36.9372149776075, -104.5126119783768 37.08163268820887, -104.5247764602161 37.29300499892048, -104.7084397427668 37.49150259925398, -104.8126599016282 37.69514285621863, -104.8452887035466 37.87613395659479, -104.7160169341003 38.05951763337799, -104.6165437927668 38.30432045855106, -104.6437227858174 38.53979986564737, -104.7596170387259 38.7322907594295, -104.8380078676822 38.89998460604341, -104.8501253693506 39.09980189213358, -104.8791648316464 39.24368776457503, -104.8635041274215 39.3785278162751, -104.8894471170052 39.5929228239605, -104.9721242843344 39.69528482419685, -105.0112104500356 39.7273080432394, -105.0010368577104 39.76677607811571, -104.981835619 39.81466504121967, -104.9858891550477 39.88806911250832, -104.9873548059578 39.98117234571016, -104.9766220487419 40.09796423450692, -104.9818565932953 40.36056530662884, -104.9912746373997 40.74904484447656)'), - ) - -stx_interstates = (('I-10', 'LINESTRING(924952.5 4220931.6,925065.3 4220931.6,929568.4 4221057.8)'), - ) diff --git a/django/contrib/gis/tests/distapp/fixtures/initial_data.json.gz b/django/contrib/gis/tests/distapp/fixtures/initial_data.json.gz new file mode 100644 index 000000000000..5151d3397786 Binary files /dev/null and b/django/contrib/gis/tests/distapp/fixtures/initial_data.json.gz differ diff --git a/django/contrib/gis/tests/distapp/tests.py b/django/contrib/gis/tests/distapp/tests.py index aacb610dcc48..4f81a91d0008 100644 --- a/django/contrib/gis/tests/distapp/tests.py +++ b/django/contrib/gis/tests/distapp/tests.py @@ -1,18 +1,17 @@ -import os, unittest +import os from decimal import Decimal from django.db import connection from django.db.models import Q -from django.contrib.gis.gdal import DataSource from django.contrib.gis.geos import GEOSGeometry, Point, LineString from django.contrib.gis.measure import D # alias for Distance from django.contrib.gis.tests.utils import oracle, postgis, spatialite, no_oracle, no_spatialite +from django.test import TestCase from models import AustraliaCity, Interstate, SouthTexasInterstate, \ SouthTexasCity, SouthTexasCityFt, CensusZipcode, SouthTexasZipcode -from data import au_cities, interstates, stx_interstates, stx_cities, stx_zips -class DistanceTest(unittest.TestCase): +class DistanceTest(TestCase): # A point we are testing distances with -- using a WGS84 # coordinate that'll be implicitly transormed to that to @@ -28,37 +27,12 @@ def get_names(self, qs): return cities def test01_init(self): - "Initialization of distance models." - - # Loading up the cities. - def load_cities(city_model, data_tup): - for name, x, y in data_tup: - city_model(name=name, point=Point(x, y, srid=4326)).save() - - def load_interstates(imodel, data_tup): - for name, wkt in data_tup: - imodel(name=name, path=wkt).save() - - load_cities(SouthTexasCity, stx_cities) - load_cities(SouthTexasCityFt, stx_cities) - load_cities(AustraliaCity, au_cities) - + "Test initialization of distance models." self.assertEqual(9, SouthTexasCity.objects.count()) self.assertEqual(9, SouthTexasCityFt.objects.count()) self.assertEqual(11, AustraliaCity.objects.count()) - - # Loading up the South Texas Zip Codes. - for name, wkt in stx_zips: - poly = GEOSGeometry(wkt, srid=4269) - SouthTexasZipcode(name=name, poly=poly).save() - CensusZipcode(name=name, poly=poly).save() self.assertEqual(4, SouthTexasZipcode.objects.count()) self.assertEqual(4, CensusZipcode.objects.count()) - - # Loading up the Interstates. - load_interstates(Interstate, interstates) - load_interstates(SouthTexasInterstate, stx_interstates) - self.assertEqual(1, Interstate.objects.count()) self.assertEqual(1, SouthTexasInterstate.objects.count()) @@ -382,8 +356,3 @@ def test09_measurement_null_fields(self): z = SouthTexasZipcode.objects.distance(htown.point).area().get(name='78212') self.assertEqual(None, z.distance) self.assertEqual(None, z.area) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(DistanceTest)) - return s diff --git a/django/contrib/gis/tests/geo3d/tests.py b/django/contrib/gis/tests/geo3d/tests.py index 034a979a4c43..a1dce9af5b7a 100644 --- a/django/contrib/gis/tests/geo3d/tests.py +++ b/django/contrib/gis/tests/geo3d/tests.py @@ -1,4 +1,6 @@ -import os, re, unittest +import os +import re +from unittest import TestCase from django.contrib.gis.db.models import Union, Extent3D from django.contrib.gis.geos import GEOSGeometry, Point, Polygon from django.contrib.gis.utils import LayerMapping, LayerMapError @@ -49,7 +51,7 @@ def gen_bbox(): bbox_3d = Polygon(tuple((x, y, z) for (x, y), z in zip(bbox_2d[0].coords, bbox_z)), srid=32140) return bbox_2d, bbox_3d -class Geo3DTest(unittest.TestCase): +class Geo3DTest(TestCase): """ Only a subset of the PostGIS routines are 3D-enabled, and this TestCase tries to test the features that can handle 3D and that are also @@ -227,8 +229,3 @@ def test07_translate(self): for ztrans in ztranslations: for city in City3D.objects.translate(0, 0, ztrans): self.assertEqual(city_dict[city.name][2] + ztrans, city.translate.z) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(Geo3DTest)) - return s diff --git a/django/contrib/gis/tests/geoapp/fixtures/initial_data.json b/django/contrib/gis/tests/geoapp/fixtures/initial_data.json deleted file mode 100644 index d8bf70b85a32..000000000000 --- a/django/contrib/gis/tests/geoapp/fixtures/initial_data.json +++ /dev/null @@ -1,97 +0,0 @@ -[ { - "pk": 1, - "model": "geoapp.country", - "fields": { - "name": "Texas", - "mpoly": "SRID=4326;MULTIPOLYGON (((-103.00243399999998 36.500397,-102.90421904194784 36.500393342967676,-102.8402359524855 36.500390960558398,-102.840235952479716 36.500390960558398,-102.840235952474345 36.500390960558398,-102.840235952468362 36.500390960558398,-102.840235952461725 36.500390960558398,-102.840235952455885 36.500390960558398,-102.792077446284736 36.500389167377229,-102.643207699865854 36.500383624214699,-102.366462330167067 36.500373319605465,-102.250452999999979 36.500368999999999,-102.24499 36.500703999999999,-102.162463000000002 36.500326,-102.12545 36.500323999999999,-102.124752896301885 36.500398159967887,-102.122066000000004 36.500684,-101.930244999999999 36.500526,-101.925344139375468 36.500484781341967,-101.914929315957153 36.500397187533892,-101.826564999999988 36.499653999999992,-101.826498 36.499535000000002,-101.81449312121488 36.499579719643286,-101.788110000000003 36.499678000000003,-101.783359000000004 36.499709000000003,-101.781987 36.499718,-101.780609999999982 36.499727,-101.779435000000007 36.499733999999997,-101.773235954788092 36.499732939140301,-101.709314000000006 36.499721999999998,-101.698684999999998 36.499507999999999,-101.653707999999995 36.499572999999998,-101.649966000000006 36.499572999999998,-101.623914999999997 36.499527999999998,-101.414793008557069 36.499417763984319,-101.392608849457574 36.499406069885133,-101.340061173170298 36.499378370041477,-101.085155999999984 36.499243999999997,-101.052418000000003 36.499562999999995,-101.04533099999999 36.499540000000003,-100.977087999999995 36.499594999999999,-100.936058000000003 36.499602000000003,-100.918513000000004 36.499620999999998,-100.884174000000002 36.499682,-100.884079999999997 36.499682,-100.859656999999984 36.499687000000002,-100.850840000000005 36.49969999999999,-100.824235999999999 36.499617999999998,-100.824218000000002 36.499617999999998,-100.80619 36.499673999999999,-100.806172000000004 36.499634,-100.802909 36.499620999999998,-100.802886 36.499620999999998,-100.761810999999994 36.499617999999998,-100.761810999999994 36.499580000000002,-100.724361999999999 36.499580000000002,-100.724361000000002 36.499558,-100.708625999999995 36.499552999999999,-100.708628000000004 36.499520999999994,-100.657762999999989 36.499482999999991,-100.657762999999989 36.499499999999998,-100.648342999999997 36.499495000000003,-100.648343999999994 36.499462999999999,-100.592613999999998 36.499468999999998,-100.592555999999988 36.499468999999998,-100.592551 36.499428999999999,-100.583539000000002 36.499482999999991,-100.583378999999994 36.499442999999999,-100.578113999999999 36.499462999999999,-100.578113999999999 36.499439000000002,-100.546144999999996 36.499343000000003,-100.531215000000003 36.499341,-100.531215000000003 36.499290000000002,-100.530478000000002 36.49924,-100.530314000000004 36.499357000000003,-100.522227 36.499290999999999,-100.441064999999995 36.499490000000002,-100.441063999999997 36.499462,-100.433959000000002 36.499456000000002,-100.421327999999988 36.499447000000004,-100.421300999999985 36.499487999999999,-100.413634000000002 36.499443999999997,-100.41355 36.499468999999998,-100.378634000000005 36.499516999999997,-100.378591999999998 36.499445,-100.35185199999998 36.499487000000002,-100.351841999999991 36.499473000000002,-100.334463999999997 36.49942,-100.334440999999998 36.49944,-100.324150000000003 36.499679,-100.311244999999985 36.499631,-100.311018000000004 36.499687999999999,-100.310642999999985 36.499642,-100.253572851827684 36.499638031344489,-100.181220999999994 36.499633000000003,-100.090020999999993 36.499634,-100.000405999999984 36.499701999999999,-100.0004059967807 36.499497793115623,-100.00040222345956 36.260147946939995,-100.000399000000002 36.055677000000003,-100.000396170268971 35.879197994164429,-100.000394020347557 35.745115995014778,-100.000391999999991 35.619115,-100.000390396259149 35.519130234823749,-100.000386875554753 35.299632926398459,-100.000386874882437 35.299591010324292,-100.000386852473085 35.298193905884737,-100.000384999999994 35.182701999999999,-100.000381972929958 34.852568985943549,-100.000381605014198 34.812443999872983,-100.000381000000004 34.746460999999996,-100.000381000000004 34.570853999999997,-100.000381000000004 34.570647,-100.000381000000004 34.560509000000003,-99.998254592787575 34.560446149085699,-99.997501461048799 34.560423888524269,-99.985833 34.560079000000002,-99.974761999999998 34.561317999999993,-99.971554999999995 34.562179,-99.965608000000003 34.565843999999998,-99.958898000000005 34.571271000000003,-99.957540999999992 34.572708999999996,-99.95755299999999 34.574168999999998,-99.956716999999998 34.576523999999992,-99.954566999999997 34.578195,-99.94571999999998 34.579273,-99.930492488760507 34.576894921075187,-99.930189904388058 34.576847666503667,-99.929333999999997 34.576714000000003,-99.923210999999995 34.574551999999997,-99.921801000000002 34.570253,-99.915771000000007 34.565975000000002,-99.914151070156151 34.564995899308187,-99.898943000000003 34.555804000000002,-99.896006999999997 34.555529999999997,-99.89376 34.554219000000003,-99.887146999999999 34.549047000000002,-99.885392392967745 34.547401436342639,-99.884583851343237 34.546643143067669,-99.87806651563541 34.540530839522475,-99.87650823800611 34.539069404005723,-99.874403 34.537095,-99.873254000000003 34.535350999999999,-99.872356999999994 34.532096000000003,-99.868953000000005 34.52761499999999,-99.867217250163094 34.525864500605081,-99.853065999999998 34.511592999999998,-99.832903999999985 34.500067999999999,-99.825324999999992 34.497596,-99.818185999999997 34.487839999999991,-99.818738999999979 34.484975999999996,-99.814544624457952 34.476663062301249,-99.814312999999984 34.476204000000003,-99.793683999999999 34.453893999999998,-99.783787954821193 34.445078397966533,-99.782985999999994 34.444364,-99.775743000000006 34.444225000000003,-99.774224411180043 34.443216449834381,-99.765598999999995 34.437488000000002,-99.764825999999999 34.436433999999998,-99.764881999999986 34.435265999999999,-99.767647999999994 34.431854,-99.767234000000002 34.430501999999997,-99.754248000000004 34.421288999999994,-99.740907000000007 34.414763,-99.735679661059166 34.413018903486261,-99.730348000000006 34.411239999999992,-99.720258999999999 34.406295,-99.719721039768913 34.405807854123296,-99.716415999999995 34.402814999999997,-99.715089000000006 34.400753999999999,-99.714231999999996 34.397821999999998,-99.714838 34.394523999999997,-99.712682 34.390928000000002,-99.707900999999993 34.387538999999997,-99.696461999999997 34.381036000000002,-99.678282999999979 34.379798999999998,-99.672337448339604 34.378003970284972,-99.671377000000007 34.377713999999997,-99.666676988468737 34.374633899592595,-99.665992000000003 34.374184999999997,-99.665404581506792 34.374094751646162,-99.662705000000003 34.373679999999993,-99.659863615570984 34.374283464835351,-99.659362000000002 34.374389999999998,-99.654194000000004 34.376519000000002,-99.649662000000006 34.379885000000002,-99.630904999999998 34.376007,-99.628541546335526 34.375150829397036,-99.624196999999995 34.373576999999997,-99.600026 34.374687999999999,-99.596322999999998 34.377136999999998,-99.587595999999991 34.385866999999998,-99.587235087741874 34.386377538370702,-99.585718954359294 34.388522226586467,-99.585441999999986 34.388914,-99.584530999999984 34.391204999999999,-99.585306000000003 34.398122,-99.584479999999985 34.407673000000003,-99.580059999999989 34.416652999999997,-99.574366999999995 34.418281,-99.569695999999993 34.418418000000003,-99.563067665646059 34.417445690943012,-99.562203999999994 34.417318999999999,-99.561530791054494 34.417028950664999,-99.555986000000004 34.414639999999999,-99.549242000000007 34.412714999999992,-99.529786 34.411451999999997,-99.528744576810823 34.411579971493573,-99.523649999999989 34.412205999999998,-99.517623999999998 34.414493999999991,-99.514279999999999 34.414034999999998,-99.499874999999989 34.409607999999999,-99.497090999999983 34.407730999999991,-99.494103999999993 34.404755000000002,-99.490425999999999 34.399693999999997,-99.487218999999982 34.397955000000003,-99.477547 34.396355,-99.477019613816751 34.396364300212412,-99.474810232095294 34.396403261641368,-99.472297823530695 34.396447566809115,-99.470968999999997 34.396470999999991,-99.463286816319666 34.393024688790533,-99.452647999999996 34.388252,-99.445020999999983 34.379891999999998,-99.440759999999997 34.374122999999997,-99.430994999999982 34.373413999999997,-99.43081720973791 34.373532661492725,-99.420432000000005 34.380464000000003,-99.408847999999992 34.372776000000002,-99.407167999999999 34.372605,-99.406018176721332 34.372844364351735,-99.404336527339453 34.373194441551952,-99.402959999999979 34.373480999999998,-99.399602999999999 34.375078999999999,-99.397253000000006 34.377870999999999,-99.396160718377317 34.383134276834824,-99.391492 34.405631,-99.393918999999997 34.415273999999989,-99.396488000000005 34.417290999999999,-99.396901999999997 34.418688000000003,-99.397009999999995 34.424002999999999,-99.395768462359129 34.43494110377263,-99.394955999999993 34.442098999999999,-99.381011 34.456935999999999,-99.376037841709419 34.458617501802458,-99.375365000000002 34.458844999999997,-99.369609999999994 34.458699000000003,-99.358795 34.455862999999994,-99.354671999999994 34.451856999999997,-99.35478257092818 34.450383391084422,-99.354837000000003 34.449657999999999,-99.356770999999995 34.446542,-99.357101999999998 34.444915000000002,-99.356712999999999 34.442143999999999,-99.350407000000004 34.437083,-99.342020261837703 34.431514649700844,-99.341336999999996 34.431061,-99.341016600261696 34.430906286427735,-99.334036999999995 34.427536000000003,-99.331050066208874 34.424666026137302,-99.328674000000007 34.422383000000004,-99.325094009617374 34.416010263301388,-99.324222000000006 34.414458000000003,-99.321411663781888 34.411055277053073,-99.319798627189357 34.409102230797522,-99.319605999999979 34.408869000000003,-99.318363000000005 34.408295999999993,-99.316372999999999 34.408204999999995,-99.308273999999997 34.410013999999997,-99.299098 34.414227999999994,-99.294647999999981 34.415373000000002,-99.289922000000004 34.414731000000003,-99.287844819286008 34.413958196831629,-99.264167 34.405149000000002,-99.261320999999981 34.403498999999996,-99.261255940568432 34.403158389836314,-99.260996786434447 34.401801622187399,-99.259199386422964 34.392391568987605,-99.258998120407611 34.391337867029385,-99.258979999999994 34.391243000000003,-99.259239630132384 34.391043961974496,-99.260703756811225 34.389921531074158,-99.261190999999997 34.389547999999998,-99.262646013804954 34.388906249865343,-99.264243556741917 34.388201635660714,-99.264508000000006 34.388084999999997,-99.271031915197597 34.387722560266795,-99.271781628057255 34.38768090955238,-99.273607516746409 34.387579471291865,-99.273957999999993 34.38756,-99.27534 34.386598999999997,-99.274925999999994 34.384903999999999,-99.271845396288441 34.382114976063626,-99.271280999999988 34.381603999999996,-99.258696 34.372633999999998,-99.254722 34.372405,-99.251407999999984 34.375079999999997,-99.248969000000002 34.375984000000003,-99.242944999999992 34.372667999999997,-99.239138083830497 34.366035888164781,-99.237232999999989 34.362716999999996,-99.237183660287812 34.362563767173611,-99.235448627455725 34.357175329079247,-99.234251999999984 34.353459,-99.233273999999994 34.344101000000002,-99.23260599999999 34.342379999999999,-99.229993999999991 34.340538000000002,-99.226152999999982 34.339725999999999,-99.221975 34.340020000000003,-99.219768999999999 34.341377,-99.217335000000006 34.341520000000003,-99.213134999999994 34.340369000000003,-99.210957832415772 34.336710386428308,-99.210716000000005 34.336303999999998,-99.20972399999998 34.324934999999996,-99.211600000000004 34.313969999999998,-99.213476 34.310671999999997,-99.213233598142949 34.308226764636807,-99.211647999999997 34.292231999999991,-99.211468055040228 34.291676209875057,-99.209990337717599 34.287112032604114,-99.209742000000006 34.286344999999997,-99.209514950796304 34.286049346749877,-99.207560999999998 34.283504999999998,-99.203681000000003 34.281925999999999,-99.200221999999997 34.281151999999999,-99.196259999999995 34.281463000000002,-99.196052113016066 34.281264951942028,-99.19571031750732 34.280939333014615,-99.195605 34.280839,-99.194569999999985 34.272424,-99.196926000000005 34.260928999999997,-99.197153 34.244298,-99.19555299999999 34.24006,-99.191138999999993 34.23234,-99.190145999999984 34.229660000000003,-99.190036000000006 34.227186000000003,-99.192076 34.222192,-99.192683000000002 34.218825000000002,-99.192104 34.216693999999997,-99.189758414718312 34.214539281858485,-99.189510999999996 34.214312,-99.188483307800709 34.214128939694163,-99.159015999999994 34.20888,-99.143985 34.214762999999998,-99.138220000000004 34.219158999999998,-99.130609000000007 34.219408,-99.128513999999996 34.218766000000002,-99.127549000000002 34.217986000000003,-99.126614000000004 34.21532899999999,-99.127525000000006 34.213771,-99.130089999999996 34.212192000000002,-99.131552999999997 34.209352000000003,-99.131884999999997 34.207382000000003,-99.129791999999995 34.204402999999999,-99.126566999999994 34.203004,-99.119203999999996 34.201746999999997,-99.108757999999995 34.203400999999999,-99.102001344898341 34.205813362825268,-99.092190999999985 34.209316,-99.08119261238302 34.211229594305671,-99.079534999999993 34.211517999999991,-99.075978000000006 34.211221000000002,-99.06646499999998 34.208404000000002,-99.060343999999986 34.204760999999991,-99.059158999999994 34.202928999999997,-99.058800000000005 34.201256,-99.058083999999994 34.200569000000002,-99.048792000000006 34.198208999999999,-99.043470999999997 34.198208,-99.040961999999993 34.200842000000002,-99.039004000000006 34.204667,-99.037458999999998 34.206454,-99.036272999999994 34.206912000000003,-99.013074999999986 34.203221999999997,-99.005790000000005 34.206646999999997,-99.002916243275195 34.208781598893673,-99.002945441244265 34.209102775846141,-99.003147197273819 34.211322087284138,-99.003432988816769 34.214465787333673,-99.000760999999997 34.217643000000002,-98.99085199999999 34.221632999999997,-98.987293999999991 34.221223000000002,-98.981363999999999 34.217582999999991,-98.978684999999984 34.210231,-98.976586999999995 34.206291,-98.974131999999983 34.203566000000002,-98.969003 34.201298999999999,-98.966301999999999 34.201323000000002,-98.962469999999996 34.204667999999998,-98.962085000000002 34.206386000000002,-98.962306999999981 34.211312,-98.960791 34.213029999999996,-98.958474999999993 34.213854999999995,-98.952512999999982 34.212649999999996,-98.950395999999984 34.21168,-98.940219999999997 34.203685999999998,-98.928145 34.192689,-98.927456000000006 34.191155000000002,-98.923128999999989 34.185977999999999,-98.920704 34.183435000000003,-98.918333000000004 34.181831000000003,-98.909348999999992 34.177498999999997,-98.892677318093561 34.17250349095427,-98.87651287834953 34.167659972141138,-98.872921999999988 34.166584,-98.871543000000003 34.165026999999995,-98.871211000000002 34.163012000000002,-98.872229000000004 34.160446,-98.874954999999986 34.157031000000003,-98.874871999999996 34.155656999999998,-98.873271000000003 34.153596,-98.868116 34.149635000000004,-98.862549999999999 34.149110999999998,-98.860124999999996 34.149912999999998,-98.858418999999998 34.152732,-98.8579 34.159627,-98.857321999999982 34.161093999999999,-98.855585000000005 34.161620999999997,-98.854264541670545 34.16164976192438,-98.831114999999983 34.162154,-98.812953999999991 34.158444000000003,-98.806809999999984 34.155901,-98.804411553093104 34.153928907629435,-98.792015000000006 34.143735999999997,-98.779288744496085 34.140194111533035,-98.773070373738221 34.138463455122455,-98.767587610783494 34.136937528280072,-98.765569999999983 34.136375999999998,-98.764702233233265 34.135780085954792,-98.763778343962841 34.135145631382905,-98.761796999999987 34.133785000000003,-98.760558000000003 34.132387999999999,-98.759485999999995 34.128881999999997,-98.759653 34.126911999999997,-98.757118934362524 34.12470437936247,-98.757036999999983 34.124633000000003,-98.753813984496389 34.124468645349353,-98.749290999999999 34.124237999999998,-98.741966000000005 34.125529999999998,-98.740191287161664 34.126850584722824,-98.739461000000006 34.127394000000002,-98.737231999999992 34.130991999999999,-98.736819999999994 34.133374000000003,-98.735471000000004 34.135207999999999,-98.734286999999995 34.135758000000003,-98.730242646678789 34.1359250861193,-98.717536999999993 34.136450000000004,-98.716104 34.135947000000002,-98.70640034274598 34.134745066348501,-98.696517999999998 34.133521000000002,-98.690291400890658 34.133167457450512,-98.690072 34.133155000000002,-98.688275858887323 34.134465065675442,-98.685589983550884 34.136424083851644,-98.676900633591032 34.14276190388366,-98.676457484208683 34.143085127260051,-98.670573548505118 34.147376740066704,-98.655654999999982 34.158257999999996,-98.650582999999997 34.163113000000003,-98.648072999999997 34.164440999999997,-98.643223000000006 34.164530999999997,-98.635729999999995 34.161617999999997,-98.634085211037615 34.161100728840964,-98.630950281399748 34.160114822001631,-98.621666000000005 34.157195000000002,-98.620507214282355 34.157012478916968,-98.617663812339856 34.156564612849806,-98.616732999999996 34.156418000000002,-98.615748434938936 34.156446107485429,-98.612133570686254 34.156549305078279,-98.611829 34.156557999999997,-98.610173632571488 34.157093658210222,-98.610154152422197 34.157099961766605,-98.608852999999996 34.157521000000003,-98.603977999999998 34.160249,-98.599789 34.160570999999997,-98.577135999999996 34.148961999999997,-98.572451 34.145091,-98.560191000000003 34.133201999999997,-98.558593000000002 34.128253999999998,-98.550916999999998 34.119334000000002,-98.536257000000006 34.107343,-98.530610999999993 34.099843,-98.528199999999998 34.094960999999998,-98.504182 34.072370999999997,-98.487068052145446 34.063003092954936,-98.486783271268621 34.06284720836274,-98.486328 34.062598,-98.483944186894661 34.06241565608709,-98.482821069121968 34.062329745958955,-98.482039999999998 34.062269999999998,-98.475065999999998 34.064269000000003,-98.449033999999983 34.073461999999999,-98.446378999999993 34.075429999999997,-98.445784000000003 34.076827000000002,-98.445590305319797 34.079232123390703,-98.445584999999994 34.079298,-98.445259148452962 34.079797720749731,-98.443724000000003 34.082152,-98.442807999999999 34.083143999999997,-98.442148160210323 34.083427517317581,-98.441104460406791 34.083875970068213,-98.440092000000007 34.084311,-98.439068026005657 34.084479541105658,-98.434822808733685 34.08517828308225,-98.432126999999994 34.085622,-98.43151641213494 34.085605425226575,-98.43092946822253 34.085589492282431,-98.428479999999993 34.085523000000002,-98.425229999999999 34.084798999999997,-98.422252999999998 34.083036999999997,-98.419995 34.082487999999998,-98.419161945519861 34.082694545588339,-98.41804644206556 34.082971120917755,-98.417812999999995 34.083029000000003,-98.414426000000006 34.085073999999999,-98.400747497198822 34.098985940284976,-98.399776999999986 34.099972999999999,-98.398505572550647 34.104180252359392,-98.39838899999998 34.104565999999998,-98.398160000000004 34.121395999999997,-98.400493999999995 34.121777999999999,-98.400966999999994 34.122236,-98.398441000000005 34.128456,-98.384381000000005 34.146317000000003,-98.381237999999996 34.149453999999999,-98.367493999999994 34.156190999999993,-98.366862955318183 34.156357896864854,-98.364023000000003 34.157108999999998,-98.34173552164826 34.153594120579292,-98.339428832109121 34.153230340726623,-98.331172907733077 34.151928328079421,-98.326986688030772 34.151268134169193,-98.325445000000002 34.151024999999997,-98.324621914768684 34.150650086831803,-98.323612587945547 34.150190341106089,-98.322580000000002 34.149720000000002,-98.320651676675396 34.148059023851737,-98.318749999999994 34.146420999999997,-98.315432139066189 34.144301906683673,-98.312023538956254 34.142124858924547,-98.300208999999995 34.134579000000002,-98.299345719035045 34.134365643147696,-98.29862570169189 34.134187693395319,-98.293901000000005 34.133020000000002,-98.280321 34.130749999999999,-98.258217018239392 34.129574098564007,-98.256467 34.129480999999998,-98.254901718278489 34.129708262798985,-98.247953999999993 34.13071699999999,-98.241012999999995 34.133102999999998,-98.225282000000007 34.127245000000002,-98.223600000000005 34.125093,-98.216463000000005 34.121820999999997,-98.203710999999998 34.117676000000003,-98.200074999999998 34.116782999999998,-98.191455000000005 34.115752999999998,-98.169120000000007 34.114170999999999,-98.157411999999994 34.120466999999998,-98.154353999999998 34.122734,-98.142753999999996 34.136358999999999,-98.136769999999999 34.144992000000002,-98.130815999999996 34.150531999999998,-98.123377000000005 34.154539999999997,-98.114506000000006 34.154727,-98.109461999999979 34.154110999999993,-98.107064999999992 34.152531000000003,-98.101937000000007 34.146829999999994,-98.092021474678845 34.132735952269094,-98.090223999999992 34.130181,-98.089754999999982 34.128211,-98.090659999999986 34.12198,-98.092421000000002 34.116917,-98.095117999999999 34.11119,-98.096177285423778 34.109455137055363,-98.096466125075011 34.108982084942461,-98.099327999999986 34.104295,-98.101378023037029 34.101786489578267,-98.103538246996251 34.099143131812454,-98.104308999999986 34.098199999999999,-98.119416999999999 34.084474,-98.121038999999996 34.081265999999999,-98.120207999999991 34.072127000000002,-98.11802999999999 34.067064999999999,-98.114587 34.06228,-98.100919767943822 34.050244792175462,-98.099096110217346 34.048638900093685,-98.096177018664264 34.044625138601674,-98.096541898037387 34.040976263553816,-98.09727167092565 34.038969381040054,-98.097731364247636 34.038509687718062,-98.098001443813914 34.038239608151798,-98.102015208841422 34.03732738850595,-98.104022094890695 34.036232725638037,-98.104083244997014 34.036133356900422,-98.105481647738245 34.033860956680144,-98.105481647738245 34.032444902147553,-98.105481647738245 34.031306746267951,-98.103616999999986 34.029206999999992,-98.088202999999993 34.005481000000003,-98.085260000000005 34.003259,-98.082839000000007 34.002412,-98.055197000000007 33.995840999999999,-98.050169864666017 33.994989457544634,-98.041117 33.993456000000002,-98.027671999999981 33.993357000000003,-98.019485000000003 33.993803999999997,-98.018481521828946 33.993960861546498,-98.005667000000003 33.995964,-97.987387999999996 33.999822999999999,-97.982805999999997 34.001949000000003,-97.978243000000006 34.005386999999999,-97.974597608872358 34.00657735007583,-97.974172999999993 34.006715999999997,-97.973934144800623 34.006593661859519,-97.971670000000003 34.005434,-97.968339999999998 34.000529999999998,-97.965354903485249 33.996992503283089,-97.963375425210828 33.994646717187933,-97.96302799999998 33.994235000000003,-97.962715090700769 33.994009516348072,-97.958325000000002 33.990845999999998,-97.955849999999998 33.990136,-97.952687999999995 33.990113999999998,-97.947571999999994 33.991053,-97.946472999999997 33.990731999999994,-97.945729999999998 33.989839000000003,-97.945949999999982 33.988395999999995,-97.952184120103468 33.971402949359636,-97.95307606469359 33.968971674482539,-97.95691699999999 33.958502000000003,-97.960351000000003 33.951928000000002,-97.965737000000004 33.947392,-97.972662 33.944527,-97.974172999999993 33.942832000000003,-97.974062000000004 33.940289,-97.972493999999998 33.937907000000003,-97.971175000000002 33.937128999999999,-97.965952999999999 33.936191,-97.963425 33.936236999999998,-97.955511 33.938186000000002,-97.954466999999994 33.937773999999997,-97.953395 33.936444999999999,-97.952679000000003 33.929482,-97.953694999999996 33.924373000000003,-97.957155 33.914453999999999,-97.960615000000004 33.910353999999998,-97.961188664141474 33.909913087050903,-97.963139679674441 33.908413554571595,-97.964461 33.907398,-97.964803587866868 33.907309441163022,-97.9693952279504 33.906122503898267,-97.969873000000007 33.905999,-97.970298415583201 33.906261144464871,-97.973142999999993 33.908014,-97.976962999999998 33.912548999999999,-97.978803999999997 33.912548,-97.979984999999999 33.911402000000002,-97.983551999999989 33.904001999999991,-97.984539999999996 33.900703,-97.984566 33.899076999999998,-97.984416725907181 33.89872544733722,-97.984025057628415 33.897803036597892,-97.98383498894799 33.897355409354304,-97.983768999999995 33.897199999999991,-97.977858999999995 33.889929000000002,-97.974177999999995 33.886642999999999,-97.967776999999998 33.882429999999999,-97.958438 33.879179,-97.951215000000005 33.878424000000003,-97.946463999999992 33.878883000000002,-97.942729999999997 33.879845000000003,-97.938801999999995 33.879891,-97.936743000000007 33.879204,-97.933120450042608 33.877388671138185,-97.918328311832767 33.869976048610916,-97.905467000000002 33.863530999999995,-97.896737999999985 33.857984999999999,-97.877386999999999 33.850236000000002,-97.871447000000003 33.849001,-97.865764999999982 33.849392999999999,-97.835425213046037 33.857383352392631,-97.834333 33.857671000000003,-97.833886750888084 33.857971936447115,-97.833218553718808 33.858422547723912,-97.806802470257153 33.876236728393856,-97.805423000000005 33.877167,-97.803472999999997 33.880189999999999,-97.801578000000006 33.885137999999998,-97.784656999999982 33.890631999999997,-97.78061799999999 33.895533,-97.779683000000006 33.899242999999998,-97.780339999999995 33.904833000000004,-97.783716999999996 33.910559999999997,-97.772672 33.914382000000003,-97.765445999999997 33.913531999999989,-97.763769999999994 33.914240999999997,-97.762914903547767 33.914953098088951,-97.761142192227695 33.916429357685161,-97.760223999999994 33.917194000000002,-97.759399000000002 33.91881999999999,-97.759833999999984 33.92521,-97.762660999999994 33.930846000000003,-97.762767999999994 33.934396,-97.752956999999995 33.937049000000002,-97.738478 33.937421,-97.736553999999998 33.936574999999998,-97.735919542935619 33.936533987763056,-97.734773489745407 33.936459905200778,-97.733722999999998 33.936391999999998,-97.732266999999993 33.936691000000003,-97.725289000000004 33.941045000000003,-97.720699142354164 33.94461309292862,-97.716772000000006 33.947665999999998,-97.714693355370301 33.94981590741822,-97.712051708440285 33.952548118711093,-97.709683999999996 33.954996999999999,-97.708350195107343 33.95701014009046,-97.70415899999999 33.963335999999998,-97.69792099999998 33.977331,-97.69310999999999 33.983699,-97.688023 33.986606999999999,-97.671772000000004 33.991370000000003,-97.661489000000003 33.990817999999997,-97.656210000000002 33.989488,-97.633778000000007 33.981256999999999,-97.609091000000006 33.968093000000003,-97.589597999999995 33.953553999999997,-97.588828000000007 33.951881999999998,-97.591270649549756 33.930345579062646,-97.591514000000004 33.928199999999997,-97.595084 33.922953999999997,-97.596154999999996 33.922105999999999,-97.59697899999999 33.920228000000002,-97.597115000000002 33.917867999999999,-97.596955673755872 33.917077348335745,-97.596288999999985 33.913769000000002,-97.593782375104212 33.910260437761373,-97.589253999999997 33.903922,-97.587440999999984 33.902479,-97.582744000000005 33.900784999999999,-97.578907598302408 33.900207204108142,-97.558269999999993 33.897098999999997,-97.555002000000002 33.897281999999997,-97.551541 33.897947000000002,-97.543245999999996 33.901288999999998,-97.533617322522971 33.906127586572126,-97.532723000000004 33.906576999999999,-97.525277000000003 33.911750999999995,-97.519170999999986 33.913637999999999,-97.504870374844515 33.918353570482601,-97.500960000000006 33.919643,-97.495648860558163 33.919307898609453,-97.494857999999979 33.919257999999999,-97.492061582842766 33.918500058129531,-97.487115891787269 33.917159576320657,-97.48650499999998 33.916993999999995,-97.484158940985964 33.915822631562747,-97.460375999999982 33.903948,-97.458068999999995 33.901634999999999,-97.451065249608234 33.891558064966901,-97.450953999999996 33.891398000000002,-97.451469000000003 33.87093,-97.452287410342421 33.86882620086994,-97.455665873415143 33.860141550511862,-97.457616999999999 33.855125999999998,-97.459565999999995 33.853316,-97.461485999999994 33.849559999999997,-97.462857 33.841771999999999,-97.459067999999988 33.834581,-97.453056999999987 33.828536,-97.444192999999999 33.823773000000003,-97.426492999999994 33.819398,-97.410387 33.818845000000003,-97.403235695189224 33.818961304668854,-97.384901622687948 33.819259479377855,-97.372940999999997 33.819454,-97.368744000000007 33.821471000000003,-97.365506999999994 33.823762999999992,-97.358512999999988 33.830018000000003,-97.348337999999984 33.843876000000002,-97.340900078515631 33.860236176367188,-97.339391593410966 33.867629740388111,-97.337846311291798 33.870430566802547,-97.336524008254173 33.872827243260353,-97.33293952159849 33.874440259476913,-97.329175814777756 33.874440259476913,-97.327562805507441 33.873902588562437,-97.326564370247013 33.872737756024428,-97.326487449786001 33.872648016149064,-97.324157539016809 33.866016724171544,-97.322365295688968 33.864941382342593,-97.318243141591921 33.865120602507631,-97.31654836796514 33.865642075591225,-97.315913230822716 33.865837504006514,-97.314412999999988 33.866988999999997,-97.310479256695203 33.873361218982971,-97.307489695517361 33.878203969770759,-97.302471419756401 33.880175433263638,-97.299245387323282 33.880175433263638,-97.295170524880618 33.877119286431629,-97.294227111562321 33.876411726442917,-97.286921603026471 33.869423850720118,-97.28598279642199 33.868525862052032,-97.284253187903744 33.867526845294535,-97.279107999999979 33.864555000000003,-97.275347999999994 33.863225,-97.271531999999993 33.862560000000002,-97.257971626565464 33.863220416657512,-97.256624999999985 33.863286000000002,-97.255635999999996 33.863697999999999,-97.254234999999994 33.865322999999997,-97.249208999999993 33.875100999999994,-97.246418496348156 33.898356425448434,-97.246179999999981 33.900343999999997,-97.245398270620598 33.902084836575838,-97.245057441245748 33.90284383100218,-97.244945999999999 33.903092,-97.242092 33.906277000000003,-97.241794392023195 33.906436890220043,-97.238755934556053 33.908069304909361,-97.226522000000003 33.914642,-97.210920999999999 33.916063999999999,-97.210512235443105 33.915911440173737,-97.206141000000002 33.914279999999998,-97.185457999999997 33.9007,-97.180845000000005 33.895203999999993,-97.179608999999999 33.892249999999997,-97.170773789281327 33.861660975771436,-97.166629 33.847310999999998,-97.166824000000005 33.840395,-97.171627 33.835335,-97.180939422624874 33.831550006302521,-97.181369999999987 33.831375,-97.186254000000005 33.830894,-97.193690000000004 33.831307000000002,-97.195830999999998 33.830803000000003,-97.197477000000006 33.829794999999997,-97.199700000000007 33.827322000000002,-97.203513999999998 33.821824999999997,-97.204994999999997 33.81886999999999,-97.205652 33.809823999999999,-97.205556910941183 33.806237292333442,-97.205445103342427 33.802019970418343,-97.205431000000004 33.801487999999999,-97.205114487332324 33.800890302957832,-97.203236000000004 33.797342999999998,-97.194785999999993 33.785344000000002,-97.190397000000004 33.781153000000003,-97.187792000000002 33.769702000000002,-97.181842999999986 33.755869999999994,-97.173532726140948 33.740090726508434,-97.172577000695028 33.738276026602087,-97.172191999999995 33.737544999999997,-97.168438536634085 33.734131892706188,-97.163454368039083 33.729599677915004,-97.163149000000004 33.729322000000003,-97.162807288850388 33.729115696596551,-97.155066000000005 33.724442000000003,-97.152609597300682 33.723370138808036,-97.150103410774264 33.72227655424301,-97.149393999999987 33.721966999999999,-97.137529999999998 33.718663999999997,-97.126102000000003 33.716940999999998,-97.121101999999993 33.717174,-97.119522126797463 33.717502594273334,-97.114921749218269 33.718459416457094,-97.113264999999998 33.718803999999999,-97.112398501853761 33.719102240295193,-97.110065570752283 33.719905212653977,-97.108936 33.720294000000003,-97.108097026518521 33.720734123472262,-97.106518853348803 33.721562029324609,-97.104524999999995 33.722608,-97.097154000000003 33.727809,-97.094085000000007 33.730992,-97.092697209381924 33.732891057656268,-97.092130098039192 33.733667094850453,-97.091071999999983 33.735115,-97.089936943375889 33.737167271747261,-97.087911027802278 33.740830286618703,-97.086195000000004 33.743932999999998,-97.084820762198987 33.752363244406453,-97.084693 33.753146999999998,-97.08461299999999 33.759993,-97.085217999999998 33.765512,-97.087362453285593 33.77250304797397,-97.087851999999998 33.774099,-97.093101265647221 33.787040841586602,-97.093917000000005 33.789051999999998,-97.094675961767678 33.791977368936216,-97.095047178759813 33.793408200769441,-97.095235999999986 33.794136000000002,-97.095080023591905 33.79561056406444,-97.094877682854474 33.797523445530629,-97.094770999999994 33.798532000000002,-97.093897185962049 33.800360798466031,-97.09266432242093 33.802941048788071,-97.092111999999986 33.804096999999999,-97.087998999999982 33.808746999999997,-97.078589999999991 33.812755999999993,-97.067977124183287 33.814475679891196,-97.064604445188394 33.815487484896828,-97.062631847535258 33.816079264957295,-97.058622892638837 33.818751903281317,-97.055415722506638 33.823829921794093,-97.055412197115942 33.8238546000754,-97.055148464371385 33.82570077017467,-97.055682980641905 33.830778788687446,-97.057821087157734 33.834520485448607,-97.058282726048489 33.836367011192308,-97.058622892638837 33.837727655580807,-97.057553829022481 33.840133030590344,-97.055415722506638 33.841202089027483,-97.052208542016004 33.841736615656437,-97.048734113748537 33.840934820533782,-97.045339940802535 33.839496399893775,-97.041245000000004 33.837761,-97.038858000000005 33.838264000000002,-97.023899 33.844213000000003,-97.017857000000006 33.850141999999998,-97.010381749407998 33.858564100233409,-96.999664164722446 33.87063922351804,-96.985567000000003 33.886521999999999,-96.983970999999997 33.892082999999992,-96.984938999999997 33.904865999999991,-96.985275905099471 33.906070041818985,-96.988744999999994 33.918467999999997,-96.993996999999979 33.928978999999998,-96.995022999999989 33.932034999999999,-96.995140238436761 33.933014648420269,-96.996024643022366 33.940404763634284,-96.996183000000002 33.941727999999998,-96.996167702736543 33.941832622020257,-96.995421139189489 33.946938567064805,-96.995367999999999 33.947302,-96.994947208134789 33.948043840473474,-96.994456760007211 33.948908482357652,-96.994287999999983 33.949205999999997,-96.990835000000004 33.952700999999998,-96.988126301178397 33.954514162310069,-96.987892000000002 33.954670999999998,-96.982723774786024 33.956016867344054,-96.981537499896135 33.956325787441237,-96.981336999999982 33.956377999999994,-96.979415000000003 33.956178,-96.979347000000004 33.955129999999997,-96.980675999999988 33.951813999999999,-96.981031000000002 33.949159999999999,-96.97981799999998 33.941588000000003,-96.976955000000004 33.937452999999998,-96.973806999999979 33.935696999999998,-96.97254199999999 33.935794999999999,-96.952313000000004 33.944581999999997,-96.944610999999995 33.949216999999997,-96.933309804512817 33.955134148312766,-96.932252000000005 33.955688000000002,-96.924267999999998 33.959159,-96.922113999999979 33.959578999999998,-96.921429967464036 33.959451233053208,-96.919039990098355 33.959004821377064,-96.918617999999995 33.958925999999998,-96.91833921313939 33.958790334953079,-96.917063225391544 33.958169405626251,-96.916299999999993 33.957797999999997,-96.91417790611122 33.956157267456653,-96.911732563120111 33.95426660943896,-96.911336000000006 33.953960000000002,-96.910857206749995 33.953482904168467,-96.908421363636307 33.951055696608989,-96.907387 33.950024999999997,-96.905252999999988 33.947218999999997,-96.902433999999985 33.942017999999997,-96.901946611333699 33.940667581536225,-96.899441999999993 33.933728000000002,-96.896468999999996 33.91331799999999,-96.897193999999999 33.902954,-96.895728000000005 33.896414,-96.887761838797758 33.878628251663962,-96.883009999999999 33.868018999999997,-96.875280999999987 33.860505000000003,-96.87198767019855 33.857765462058182,-96.866438000000002 33.853149000000002,-96.85608999999998 33.84749,-96.850593000000003 33.847211,-96.845895999999996 33.848974999999996,-96.841592000000006 33.852893999999999,-96.840818999999996 33.863644999999998,-96.839777999999995 33.868395999999997,-96.837412999999984 33.871349000000002,-96.833926235310372 33.873661568818115,-96.832156999999995 33.874834999999997,-96.812777999999994 33.872646000000003,-96.794275999999996 33.868886000000003,-96.786859371055769 33.865207582975678,-96.783484999999999 33.863533999999994,-96.780568999999986 33.860098,-96.779588000000004 33.857939000000002,-96.777202000000003 33.848162000000002,-96.776765999999995 33.841976000000003,-96.770675999999995 33.829621000000003,-96.769378000000003 33.827477000000002,-96.766316855179326 33.825510582121247,-96.766234999999995 33.825457999999998,-96.761588000000003 33.824406000000003,-96.754041 33.824657999999999,-96.746233969152598 33.825673509073113,-96.746037999999984 33.825699,-96.741799282455162 33.826447231494264,-96.71318112067361 33.831498997677379,-96.712421999999989 33.831632999999997,-96.708134 33.833060000000003,-96.704457000000005 33.835020999999998,-96.700318655907978 33.838434731313264,-96.699573999999998 33.839049000000003,-96.695480719177567 33.844085960723298,-96.693127324791789 33.84698191524042,-96.690708 33.849958999999998,-96.688190999999989 33.854613,-96.687524326814838 33.85620885855986,-96.684726999999995 33.862904999999998,-96.682762564556768 33.871464102957781,-96.682209 33.873876000000003,-96.682102999999998 33.876645000000003,-96.683464 33.884217,-96.681051007513375 33.895708672998403,-96.680947000000003 33.89620399999999,-96.675306000000006 33.909114000000002,-96.673449000000005 33.912278,-96.670618000000005 33.914913999999996,-96.667186999999998 33.916939999999997,-96.665308436653305 33.917161206414967,-96.66440999999999 33.917267000000002,-96.659896000000003 33.916665999999999,-96.65150600931733 33.910998546998165,-96.644049999999979 33.905962000000002,-96.630116999999998 33.895422000000003,-96.628293999999997 33.894477000000002,-96.616355751891987 33.894625565889051,-96.614680358047494 33.89464641537834,-96.611821556077771 33.89468199182577,-96.607562 33.894734999999997,-96.592948000000007 33.895615999999997,-96.588319642127203 33.894847991673281,-96.587934000000004 33.894784,-96.58760387584924 33.894318075382706,-96.585452000000004 33.891280999999999,-96.585359999999994 33.888947999999999,-96.587494000000007 33.884250999999999,-96.590112000000005 33.880665,-96.597347999999982 33.875100999999994,-96.601685999999987 33.872822999999997,-96.611969999999999 33.869016000000002,-96.625399000000002 33.856541999999997,-96.628968999999998 33.852406999999999,-96.629746999999995 33.850866000000003,-96.630021999999983 33.847541,-96.629842405402627 33.847037300944812,-96.629577623992816 33.846294683138318,-96.629289999999997 33.845488000000003,-96.627812273090953 33.844523322531259,-96.626623854929747 33.84374750920842,-96.623154999999997 33.84148299999999,-96.622548607895212 33.841284829387497,-96.620258613641042 33.840536452948584,-96.605267599881515 33.835637348301233,-96.601258 33.834327000000002,-96.599141379811712 33.83346048638235,-96.597030984690946 33.832596521217091,-96.595164098773168 33.831832245189069,-96.592925999999991 33.830916000000002,-96.587067000000005 33.828009000000002,-96.573054340771023 33.81917200025552,-96.572936999999996 33.819097999999997,-96.568822993124115 33.818734252140963,-96.566549213959448 33.818533211567136,-96.566298000000003 33.818511,-96.551222999999993 33.81912899999999,-96.541502252916899 33.82118138128849,-96.537683599711926 33.821987629236112,-96.532865 33.823005000000002,-96.532141353898353 33.822830017549641,-96.529233999999988 33.822127000000002,-96.526655000000005 33.820891000000003,-96.526331240434345 33.820568979830284,-96.523863000000006 33.818114,-96.519910999999993 33.811346999999998,-96.517492044660074 33.805400310572544,-96.516583999999995 33.803167999999999,-96.515958999999995 33.798933999999996,-96.515952403498957 33.797370629253059,-96.5159410675722 33.794684014612457,-96.515912 33.787795000000003,-96.51191399999999 33.781477999999993,-96.502285999999998 33.773459999999993,-96.500268000000005 33.772582999999997,-96.486059999999995 33.773009999999999,-96.459153999999998 33.775232000000003,-96.456254 33.776035,-96.450509999999994 33.780588000000002,-96.448044999999993 33.781030999999999,-96.436454999999995 33.780050000000003,-96.430214000000007 33.778654000000003,-96.423664429058576 33.776393528613141,-96.422642999999994 33.776040999999999,-96.422322840041474 33.775682043625913,-96.420980388055867 33.774176915691271,-96.419961 33.773034000000003,-96.419583102042878 33.772404537625398,-96.417562000000004 33.769038000000002,-96.416145999999998 33.76609899999999,-96.413408000000004 33.757714,-96.411885201063754 33.755703128434462,-96.408468999999997 33.751191999999996,-96.403507000000005 33.746288999999997,-96.384116000000006 33.730141000000003,-96.383299027856694 33.729391180874664,-96.380090129695702 33.726446045924753,-96.379450023740389 33.7258585550397,-96.370956555983966 33.718063228581727,-96.370761536889887 33.717884239557762,-96.369590000000002 33.716808999999998,-96.369084597701828 33.715741445126696,-96.366945 33.711221999999999,-96.363253 33.701050000000002,-96.363143372320295 33.69469995601051,-96.363135 33.694215,-96.362964209763192 33.693778090504111,-96.362198000000006 33.691817999999998,-96.356236230636398 33.68737146600408,-96.355945999999989 33.687154999999997,-96.355455421824701 33.68710517164083,-96.352724507664888 33.686827790830883,-96.348305999999994 33.686378999999995,-96.346643697131185 33.686554630652331,-96.344174978865311 33.686815463144171,-96.342664999999997 33.686974999999997,-96.337175125435976 33.689043696356201,-96.321102999999994 33.695099999999996,-96.318759999999997 33.696753,-96.318590686098233 33.696960051986665,-96.318010443675675 33.697669623646739,-96.316924999999998 33.698996999999999,-96.314798583555884 33.702507526903553,-96.309963999999994 33.710489000000003,-96.308342766341951 33.715746247280322,-96.307034999999985 33.719987000000003,-96.307001890234133 33.720499786556005,-96.306595999999999 33.726785999999997,-96.307389 33.735005,-96.3061 33.741002000000002,-96.30525491425243 33.743702118680943,-96.304087485467235 33.747432149959735,-96.303009000000003 33.750878,-96.301705999999982 33.753756000000003,-96.294866999999996 33.764771000000003,-96.292482000000007 33.766418999999999,-96.277269000000004 33.769734999999997,-96.269895999999989 33.768405,-96.251497151236435 33.760405611313516,-96.248231999999987 33.758986,-96.23960043273749 33.754058875473291,-96.229595766085112 33.748347949873668,-96.229022999999998 33.748021,-96.220521000000005 33.747390000000003,-96.1999 33.752116999999998,-96.196336040977513 33.753254070097242,-96.186553999999987 33.756374999999998,-96.178059000000005 33.760517999999998,-96.174632999999986 33.763699000000003,-96.169932696734321 33.769534234627457,-96.169452000000007 33.770130999999999,-96.167888847884015 33.774482610028038,-96.162756999999985 33.788769000000002,-96.162122572851359 33.796140263149638,-96.162213638666273 33.796174412747511,-96.166837334419654 33.79790829445497,-96.170373408451042 33.799381659586444,-96.173025461119408 33.800560352833699,-96.175150000000002 33.801951000000003,-96.17734 33.805117000000003,-96.17895107878303 33.810509748931381,-96.178963999999993 33.810552999999999,-96.176910000000007 33.813934000000003,-96.175889999999981 33.814627000000002,-96.164217431250009 33.817001137011715,-96.150765000000007 33.816986999999997,-96.148792 33.819197000000003,-96.151629999999983 33.831945999999995,-96.150147000000004 33.835856,-96.148457006953691 33.837436961236868,-96.14806999999999 33.83779899999999,-96.147446683377808 33.837891494337825,-96.138904999999994 33.839159000000002,-96.128108733462625 33.839703753325978,-96.122951 33.839963999999995,-96.118168999999995 33.837883999999995,-96.109992999999989 33.832396000000003,-96.104074999999995 33.830730000000003,-96.099360000000004 33.830469999999998,-96.097448 33.832724999999996,-96.097637999999989 33.837935000000002,-96.099152999999987 33.842409000000004,-96.100785000000002 33.844230000000003,-96.101348999999999 33.845720999999998,-96.101472999999999 33.846708999999997,-96.100094999999996 33.847971,-96.084626 33.846656000000003,-96.063924 33.841522999999995,-96.055357999999984 33.838262,-96.049381559404907 33.836618570443349,-96.048833999999999 33.836468000000004,-96.044074587870469 33.838420736557822,-96.037191000000007 33.841245,-96.031783693249977 33.849934429642978,-96.031271075997878 33.850758194920559,-96.029462717056688 33.852402158173604,-96.025188419607474 33.852073366797306,-96.022229284477689 33.850922593794486,-96.021900493101384 33.849114234853282,-96.022507000000004 33.846130000000002,-96.022064891975305 33.843195970965255,-96.021407302851145 33.841880802274289,-96.019950829321616 33.840821548098475,-96.019598947095744 33.84056563358331,-96.005296 33.845505000000003,-96.000534603789163 33.849305889934179,-95.998351 33.851049000000003,-95.997709 33.852181999999992,-95.997673906338932 33.852568581878202,-95.997405462294296 33.855525685805766,-95.9977342536706 33.860950759443568,-95.997376655326022 33.862202357114477,-95.996747879541701 33.864403078452035,-95.993624351909531 33.866211440579008,-95.991487204777812 33.866869023331596,-95.988856861024331 33.866869023331596,-95.984753921632091 33.864671017651808,-95.984253769013037 33.864403078452035,-95.980965842506947 33.859306796190523,-95.9735403792467 33.856832331211713,-95.972155999999998 33.856371000000003,-95.971744371228908 33.856383941655046,-95.951609000000005 33.857016999999999,-95.945502988542614 33.859346036998218,-95.944283999999982 33.859811,-95.943359355023844 33.860365112733476,-95.941777921153459 33.861312819872232,-95.941266999999996 33.861618999999997,-95.936631000000006 33.870615,-95.93550004724635 33.874497995518659,-95.935325000000006 33.875098999999999,-95.935308000000006 33.878723999999998,-95.935637 33.880370999999997,-95.936817000000005 33.882385999999997,-95.937201999999999 33.884652000000003,-95.936131999999986 33.886825999999999,-95.935198 33.887100999999994,-95.922712000000004 33.883758,-95.915960999999996 33.881148000000003,-95.905343000000002 33.875629000000004,-95.893305999999995 33.868161,-95.887490999999997 33.863855999999998,-95.881292000000002 33.860627,-95.859469000000004 33.852455999999997,-95.849863999999997 33.844951999999999,-95.843772999999999 33.838949,-95.841300369514457 33.837329726167006,-95.840012000000002 33.836486,-95.839442445999168 33.836292954052603,-95.838335071878717 33.835917618112738,-95.837515999999994 33.835639999999998,-95.831947999999997 33.835160999999999,-95.828244999999995 33.836053999999997,-95.827967044704152 33.836191602640042,-95.826538854622555 33.836898632614485,-95.822787000000005 33.838755999999989,-95.821905415666805 33.839551758599306,-95.821112514659319 33.840267467546653,-95.820784000000003 33.840564,-95.819357999999994 33.842784999999999,-95.818975999999992 33.844456,-95.819524999999999 33.848438999999999,-95.820676999999989 33.850750999999995,-95.821665999999979 33.855443,-95.821665999999979 33.856633000000002,-95.82138191533943 33.857119395418813,-95.820824189768416 33.858074304994595,-95.820595999999995 33.858464999999995,-95.820256321913618 33.858527429344676,-95.805149 33.861303999999997,-95.800842000000003 33.861212000000002,-95.790314669347865 33.857829825250164,-95.789867 33.857685999999994,-95.789358977006231 33.85733891951336,-95.788304168820886 33.85661827626933,-95.787891000000002 33.856335999999999,-95.776255000000006 33.845145000000002,-95.773281999999995 33.843834,-95.772067000000007 33.843817,-95.763621999999984 33.847954,-95.758311274366889 33.849968021173019,-95.758015999999998 33.850079999999998,-95.757640875431136 33.850475976069447,-95.754310000000004 33.853991999999998,-95.753512999999984 33.856464000000003,-95.753688987366843 33.856976705401074,-95.75729390188404 33.867478931648478,-95.757457999999986 33.86795699999999,-95.758009454152003 33.868443703186436,-95.760805000000005 33.870911,-95.762558999999996 33.874366999999992,-95.76194240701966 33.883030946465368,-95.761915999999999 33.883401999999997,-95.758343999999994 33.890610999999993,-95.756366999999997 33.892625000000002,-95.747335000000007 33.895755999999999,-95.737508000000005 33.895966999999999,-95.729445045960333 33.893952819075864,-95.728448999999998 33.893704,-95.723226300470785 33.890698381785455,-95.717160861060378 33.887207774089354,-95.713910181462765 33.885337036216413,-95.713539999999981 33.885123999999998,-95.710877999999994 33.884551999999999,-95.696961999999999 33.885218000000002,-95.684831000000003 33.890231999999997,-95.676924999999983 33.897236999999997,-95.669978 33.905844000000002,-95.665338000000006 33.908132000000002,-95.659818 33.909092,-95.647272999999998 33.905976000000003,-95.636977999999999 33.906613,-95.609439392746168 33.923623282239362,-95.603656999999998 33.927194999999998,-95.599677999999983 33.934246999999999,-95.585944999999981 33.93448,-95.570311428427317 33.932892416047835,-95.564667905744443 33.932319318211334,-95.563423999999998 33.932192999999998,-95.562724847986416 33.932007581530534,-95.561592737930383 33.931707340510293,-95.561007000000004 33.931551999999996,-95.560415995740641 33.931042615914556,-95.559931936825777 33.930625407571753,-95.559414000000004 33.930179000000003,-95.558286100391328 33.928753215740784,-95.557777967458279 33.928110882033096,-95.556914999999989 33.927019999999999,-95.551147999999984 33.914566,-95.549144999999996 33.90795,-95.549475 33.901310999999993,-95.552330999999981 33.894419999999997,-95.55209457427955 33.888655441173519,-95.552085000000005 33.888421999999998,-95.551943532854764 33.888208369560985,-95.548485831718622 33.882986873004867,-95.548324999999991 33.882744000000002,-95.547838962671932 33.882363312195096,-95.545708294665019 33.880694470565629,-95.545197000000002 33.880293999999999,-95.54352178779898 33.880173169084813,-95.541265054919734 33.880010393826282,-95.539789999999996 33.879904000000003,-95.537358049249164 33.88037416967029,-95.534038375697051 33.881015963020303,-95.533282999999997 33.881161999999996,-95.531798241123994 33.881968630089013,-95.525321999999989 33.885486999999998,-95.521418133191162 33.888379988113826,-95.520137966641357 33.88932866459119,-95.515301759339067 33.891142240377064,-95.510062536063273 33.890134704348199,-95.506233872599807 33.886306036979761,-95.506495 33.878588999999998,-95.506084999999999 33.87639,-95.502407477038815 33.874787101867227,-95.502303999999995 33.874741999999998,-95.492028000000005 33.874822000000002,-95.480004545492946 33.87882505156746,-95.478574999999992 33.879300999999998,-95.477829321179073 33.879890044047791,-95.469962325642697 33.886104525088022,-95.467351237093553 33.886417854985297,-95.464924614258621 33.886709049048328,-95.462909526581015 33.885903021006229,-95.461498966768687 33.883686425341857,-95.46277824472287 33.878821065729895,-95.464211000000006 33.873372000000003,-95.463346 33.872312999999998,-95.461127233926518 33.871832054399569,-95.447370000000006 33.868850000000002,-95.418546279096461 33.866998581211959,-95.411345060268999 33.866536029139695,-95.407794999999979 33.866307999999989,-95.406882133018115 33.866362247208706,-95.404325916920058 33.866514150597617,-95.375232999999994 33.868243,-95.352338000000003 33.867789000000002,-95.339561303488324 33.868836967540759,-95.339121999999989 33.868873,-95.339014688391742 33.869073090388603,-95.33835013321854 33.870312202400832,-95.334854000000007 33.876830999999996,-95.334836460323601 33.877305631062114,-95.334523000000004 33.885787999999998,-95.333451999999994 33.886285999999998,-95.325571999999994 33.885703999999997,-95.310579050797429 33.880679668661742,-95.294789354523203 33.875388337067108,-95.287864786488143 33.87494634339312,-95.28344485260331 33.877745636185885,-95.281676877907344 33.882902226669891,-95.281529544303439 33.887616824907447,-95.281317419351268 33.889260797911334,-95.280350898312903 33.896751357029835,-95.279761569607459 33.899108654721076,-95.277846267017765 33.900876629417048,-95.275341635722626 33.901760616765031,-95.272542342929853 33.902055281117747,-95.267212735027385 33.900338966530455,-95.263849803053915 33.899255988324974,-95.261050510261143 33.899992642069066,-95.255746586173245 33.902939265610648,-95.253094626984378 33.90544389690578,-95.250884657186859 33.913105118684925,-95.250737329293145 33.917083057468233,-95.251326652288412 33.924154956252103,-95.253020000000006 33.927236999999998,-95.253623000000005 33.92971,-95.252905999999982 33.933647999999998,-95.250453815364224 33.936614470603772,-95.23166760862577 33.959340626388752,-95.231194814374604 33.959912577712174,-95.230491 33.960763999999998,-95.226393000000002 33.961953999999999,-95.219358 33.961567000000002,-95.184075000000007 33.950353,-95.174181557651636 33.944707625858101,-95.173657196254922 33.944408415920272,-95.168745999999999 33.941605999999993,-95.166685999999984 33.939728000000002,-95.161108999999982 33.937598,-95.149462 33.936335999999997,-95.131725657216535 33.936903570678005,-95.131056 33.936924999999995,-95.130760550144942 33.936820411866918,-95.124700000000004 33.934674999999991,-95.121184 33.931306999999997,-95.121974867397924 33.925542192115131,-95.122499622273722 33.921717137430115,-95.122365486317321 33.918632002634986,-95.119951031304154 33.915815139752631,-95.110963896232136 33.912998276870283,-95.103318123323447 33.913668959251616,-95.10214780889666 33.912991409673538,-95.100769532353866 33.912193461131935,-95.098489218495843 33.909913139475762,-95.095001673232161 33.904815960136006,-95.095247706914634 33.899772255341666,-95.095269945144935 33.899316370327696,-95.09392858558104 33.895962961020395,-95.090441035118587 33.893280234094433,-95.084002493615529 33.893280234094433,-95.079139333602001 33.898143394107954,-95.078905311676394 33.898377416033576,-95.074957661870556 33.900039583313315,-95.073630040977179 33.900598581227868,-95.071259538767663 33.901596686785098,-95.065491679645973 33.899584642240477,-95.06106518008815 33.895292278639054,-95.058834000000004 33.886812999999997,-95.049025 33.864089999999997,-95.046567999999994 33.862564999999996,-95.038661112238941 33.860609605793528,-95.037206999999995 33.86025,-95.033518790033554 33.860141698175291,-95.03143098776043 33.86008039125462,-95.030255193062573 33.860045864827867,-95.022324999999995 33.859813000000003,-95.016999191469623 33.861237606415294,-95.016422000000006 33.861392000000002,-95.008375999999984 33.866088999999995,-95.000223000000005 33.862504999999999,-94.995524000000003 33.857438000000002,-94.992810330111539 33.852698351540766,-94.992671 33.852454999999999,-94.992523097811741 33.852403566519136,-94.990494037024106 33.851697953840834,-94.988486999999992 33.850999999999999,-94.98739725328582 33.851074415574232,-94.983303000000006 33.851354,-94.981650000000002 33.852283999999997,-94.976208 33.859846999999995,-94.973410999999999 33.861730999999999,-94.971435 33.862122999999997,-94.968895000000003 33.860916000000003,-94.965888000000007 33.848421999999999,-94.964400999999995 33.837020999999993,-94.957676000000006 33.835003999999998,-94.949533421399437 33.825707838785306,-94.949112883549958 33.821754768331147,-94.948729542346143 33.818151347643564,-94.948715939727023 33.818023482549535,-94.944301522854289 33.81213759866646,-94.939560116480934 33.810502632153295,-94.935799688114457 33.810339132650462,-94.932366255585293 33.810993121156741,-94.928442330884351 33.812628087669893,-94.92451840618341 33.812791587172725,-94.924196338046286 33.812670811231236,-94.921902464831703 33.811810605997493,-94.919450010309433 33.810175636315982,-94.917815040627914 33.808704169305649,-94.917091634856007 33.805689966907131,-94.916834062621035 33.804616745101868,-94.916997555787162 33.801510305241678,-94.91961350347556 33.794152948011565,-94.920034399525505 33.790224602097751,-94.920103995647338 33.789575041141042,-94.920095560257593 33.789518805381888,-94.91961350347556 33.786305103362189,-94.913003475392784 33.779908559849432,-94.912450337640749 33.77937328682863,-94.911427000000003 33.778382999999998,-94.906244999999998 33.778191999999997,-94.902276 33.776288999999998,-94.89497736748055 33.771540270803243,-94.89019868072495 33.768431100796676,-94.888367999999986 33.76724,-94.887910246679809 33.766674529711281,-94.886225692565191 33.764593571999796,-94.885411379435553 33.764756432942491,-94.881447983999394 33.765549103837166,-94.879218389137634 33.764912090842074,-94.876033253179955 33.760771407616105,-94.875653319149677 33.757021753168047,-94.875534806260347 33.755852122792213,-94.875497398046861 33.755482932714841,-94.877080000000007 33.75222,-94.874668 33.749164,-94.870299604394191 33.746484207390097,-94.869443369718425 33.745958950164457,-94.869299999999996 33.745871,-94.8570941233355 33.742035460072337,-94.849295999999981 33.739584999999991,-94.841633779899112 33.739430990835892,-94.830804318260235 33.740068022348083,-94.828875382311935 33.74092532536806,-94.827937698058648 33.741342073027738,-94.826026615866809 33.743890180559411,-94.824752565187154 33.749304914465036,-94.82447272313469 33.749460382205008,-94.821885938813196 33.750897483986968,-94.817426749089677 33.752171534666616,-94.815635414888177 33.752066164468857,-94.813744972719192 33.751954964523563,-94.81275807295215 33.751896912919612,-94.812012015184067 33.751853028169073,-94.80914539498248 33.749304914465036,-94.798634446013509 33.744527205899239,-94.789716054221742 33.746119775421171,-94.777638928565466 33.753471071068567,-94.775064434371529 33.755038154868203,-94.770923763490302 33.754401129528375,-94.768057130944001 33.753445597691005,-94.766464573766783 33.750897483986968,-94.766146067269247 33.748030863785381,-94.766818924318841 33.746124416643802,-94.768057130944001 33.742616129879757,-94.767738636791165 33.737519908644046,-94.767244154304578 33.736926531576607,-94.76296091588064 33.731786662068515,-94.759138751496963 33.729557067206756,-94.758159820145337 33.729557067206756,-94.753087004596253 33.729557067206756,-94.750011775433265 33.728811557489699,-94.742576055627282 33.727008959675082,-94.742176780415733 33.726449974997927,-94.739390916583417 33.72254976995157,-94.737479828219207 33.716179498036198,-94.737788227808608 33.71500758055268,-94.73907239774114 33.71012773879076,-94.73746132453816 33.705563045258067,-94.73716130937693 33.704713004885136,-94.732383613155861 33.700253815161624,-94.728242929929891 33.699616789821789,-94.725694828570582 33.702483410023376,-94.724525908047042 33.704821269192259,-94.724102271393392 33.705668549067234,-94.721872664186904 33.707261118589173,-94.719006043985303 33.708216656598914,-94.714865360759347 33.707261118589173,-94.711043208720398 33.705668549067234,-94.709450639198465 33.699616789821789,-94.710289486124168 33.697309952648205,-94.710724689878106 33.696113138108018,-94.710724689878106 33.691653948384499,-94.710106194192988 33.688252279047049,-94.71008765219355 33.688150299756906,-94.707858069676533 33.686876245991066,-94.691547961569825 33.685092048879412,-94.684792000000002 33.684353000000002,-94.659166999999997 33.692138,-94.652265035786613 33.690979104454883,-94.649628372726468 33.688049476940073,-94.649099284135872 33.686462209886628,-94.648456523423718 33.684533926193204,-94.64818493525776 33.682632803768527,-94.647928021538007 33.680834402751707,-94.647870600191638 33.680432452214262,-94.648456523423718 33.673401362074948,-94.647827859209684 33.672301196274027,-94.646112824818204 33.669299876741562,-94.642890235687361 33.668420997570671,-94.635273213800133 33.669885811328072,-94.631328876730365 33.67284406472757,-94.630585813750528 33.673401362074948,-94.627656194751552 33.677795791992715,-94.62121101648988 33.681018386800773,-94.616816575217669 33.679553573043371,-94.614284398758912 33.677302743466427,-94.611543254774574 33.674866164477912,-94.611424034353576 33.674789522931938,-94.607441775118403 33.672229504256364,-94.60304734520065 33.671350625085473,-94.596895128555005 33.671350625085473,-94.593672545101384 33.673987285307021,-94.590449961647764 33.677502836053897,-94.590316660066918 33.67755410593854,-94.586641449284855 33.678967649811298,-94.579620000000006 33.677622999999997,-94.576973687569549 33.673401362074948,-94.5728722135906 33.669885811328072,-94.571305222138406 33.668005422800071,-94.569942586075811 33.666370260581196,-94.569356662843745 33.663440621711956,-94.571445361022185 33.660423619728157,-94.571993323065286 33.659632120703485,-94.572286279004103 33.656995454804722,-94.570821465246695 33.654944723492456,-94.568770728257221 33.65465176187643,-94.564669254278257 33.655823608340576,-94.563858010166513 33.65591721375597,-94.557052229552454 33.656702498865904,-94.552071876402621 33.653479904057846,-94.551192985877293 33.650257320604226,-94.551311999999982 33.644569999999995,-94.553536678805585 33.642054372646328,-94.552657799634687 33.638245860283412,-94.549142248887819 33.635902161677912,-94.543868917090279 33.635902161677912,-94.538888563940446 33.637952898667386,-94.538195599937964 33.637916426989378,-94.537583502619341 33.637884211439605,-94.533322276204103 33.637659937051346,-94.529220802225154 33.63443734792051,-94.528408204203203 33.630103504051213,-94.528341911699826 33.629749945032266,-94.52980672545722 33.627406252103981,-94.528927834931892 33.621839964367631,-94.526291174710352 33.619203298468861,-94.521363727720257 33.61686924674752,-94.520724886974008 33.61656663824732,-94.504615 33.620682000000002,-94.493418698303032 33.624467326832118,-94.492501061148843 33.624777568252533,-94.491502999999994 33.625115,-94.491295462081894 33.62531395337146,-94.487514000000004 33.628939000000003,-94.485874999999993 33.637867,-94.481313 33.638818999999998,-94.476415000000003 33.638947000000002,-94.466075000000004 33.636262000000002,-94.464185999999998 33.637655000000002,-94.461453000000006 33.643615999999994,-94.459197999999986 33.645145999999997,-94.454819999999998 33.644902999999999,-94.448637000000005 33.642766000000002,-94.446871000000002 33.640177999999999,-94.447513999999998 33.636254999999991,-94.448451000000006 33.634497000000003,-94.458816999999982 33.632444,-94.462736000000007 33.63091,-94.461129 33.625414999999997,-94.460285999999996 33.624420999999998,-94.45525499999998 33.622917,-94.452710999999994 33.622621000000002,-94.452325000000002 33.618817,-94.452961000000002 33.616985999999997,-94.454768999999999 33.615155999999992,-94.462335999999993 33.610567000000003,-94.469451000000007 33.60731599999999,-94.472166 33.604199,-94.471974000000003 33.602665000000002,-94.471151999999989 33.601588,-94.468086 33.599435999999997,-94.461794602929331 33.598691554192769,-94.458900358388505 33.598349085232492,-94.458231999999995 33.598269999999999,-94.454858239556387 33.593453869357283,-94.453995999999989 33.592222999999997,-94.453435550445548 33.592019500625121,-94.452150252013013 33.591552808439438,-94.451622 33.591360999999999,-94.449112 33.590893999999992,-94.442363999999998 33.591242999999999,-94.441536999999997 33.591501999999991,-94.439518000000007 33.594154000000003,-94.430358101189597 33.59122600196271,-94.430038999999994 33.591124,-94.429672337173542 33.590855074196774,-94.427920122643982 33.589569927010331,-94.427577999999983 33.589319000000003,-94.425982000000005 33.586424999999998,-94.413155000000003 33.569367999999997,-94.412481642075306 33.56890283335202,-94.412480771235352 33.568902231761562,-94.412480202418593 33.568901838813659,-94.412175000000005 33.568691,-94.408900999999986 33.568196999999998,-94.403341999999995 33.568424,-94.397341999999981 33.571607999999998,-94.385926999999995 33.581887999999999,-94.382886999999997 33.58326799999999,-94.379649 33.580607,-94.378075999999993 33.577019,-94.377759999999981 33.574609000000002,-94.378561000000005 33.571328999999999,-94.380090999999993 33.568942999999997,-94.382534000000007 33.567056999999998,-94.388052000000002 33.565511,-94.392357000000004 33.565286999999998,-94.394655618773015 33.564059042448399,-94.397398454287298 33.562313601959417,-94.399227012370631 33.559903231990447,-94.399393244337958 33.557077279687135,-94.399143896386974 33.555498071165481,-94.397957000000005 33.554389999999998,-94.392572999999999 33.551141999999999,-94.389515000000003 33.546778000000003,-94.386086000000006 33.544922999999997,-94.381666999999993 33.544035,-94.373392999999993 33.544471,-94.371597999999992 33.545000999999999,-94.363297000000003 33.544956999999997,-94.361350999999999 33.544612999999998,-94.358969999999999 33.54323,-94.355945000000006 33.54318,-94.348944999999986 33.548358999999998,-94.347382999999979 33.55107799999999,-94.34729 33.552197,-94.352653000000004 33.560611000000002,-94.352433000000005 33.56217199999999,-94.345512999999997 33.567312999999999,-94.344023000000007 33.567824000000002,-94.340576999999996 33.567878,-94.340047258358467 33.56768232744934,-94.338972839399958 33.567285465504582,-94.33842199999998 33.567081999999999,-94.337996277272822 33.566655299162022,-94.334939999999989 33.563592,-94.334379999999996 33.562536,-94.333929800646288 33.557825151092565,-94.333894999999998 33.557461000000004,-94.333202999999997 33.555365999999992,-94.331833000000003 33.553348,-94.33059 33.552692,-94.323660000000004 33.549835000000002,-94.319491999999997 33.548864000000002,-94.309582000000006 33.551673,-94.30641 33.555616,-94.306214999999995 33.557676,-94.307180999999986 33.559797000000003,-94.303577000000004 33.56828,-94.301022999999986 33.573022000000002,-94.298392000000007 33.576217999999997,-94.293257999999994 33.580418999999999,-94.289129000000003 33.582143999999992,-94.287025 33.582410000000003,-94.283581999999996 33.581890999999999,-94.282647999999995 33.580978000000002,-94.280849000000003 33.577187000000002,-94.280604999999994 33.574907999999994,-94.282171999999989 33.572989,-94.290372000000005 33.567905000000003,-94.291686999999996 33.563481000000003,-94.290901000000005 33.558872,-94.289439999999999 33.557634999999998,-94.287571999999997 33.557178,-94.279089999999997 33.557026,-94.275600999999995 33.557963999999998,-94.274473 33.558652000000002,-94.271997999999996 33.561517999999992,-94.270978999999997 33.563220999999999,-94.270853000000002 33.564782999999998,-94.265668999999988 33.573588999999998,-94.262754999999999 33.577354,-94.257801 33.582507999999997,-94.257524288354617 33.582703553652586,-94.252656000000002 33.586143999999997,-94.245931999999996 33.589113999999995,-94.242777000000004 33.589708999999999,-94.240178999999998 33.589536000000003,-94.236971999999994 33.587411000000003,-94.236362999999997 33.585991999999997,-94.236835999999997 33.580914,-94.237975000000006 33.577756999999998,-94.238867999999997 33.576721999999997,-94.244365999999999 33.573549,-94.251108000000002 33.56528,-94.252330999999984 33.561855,-94.252283000000006 33.560445,-94.251569000000003 33.558188,-94.250197 33.556764999999999,-94.237903999999986 33.552675,-94.233128263729824 33.552212399803537,-94.233017493976078 33.552201670126067,-94.231843999999981 33.552087999999998,-94.226392000000004 33.552911999999999,-94.222920999999999 33.554088,-94.21922099999999 33.556095999999997,-94.213604000000004 33.563133999999998,-94.21268334130005 33.563763266722709,-94.208078 33.566910999999998,-94.205634000000003 33.567228999999998,-94.203593999999981 33.566546000000002,-94.202594978382763 33.562850001484037,-94.201237000000006 33.557825999999999,-94.199485999999993 33.556085000000003,-94.197816999999986 33.555238000000003,-94.196394999999995 33.555123000000002,-94.193247999999997 33.556153999999999,-94.191332999999986 33.55766599999999,-94.189883999999992 33.562454000000002,-94.192482999999996 33.570425,-94.194399000000004 33.573678,-94.196366991504703 33.574779501237479,-94.201105911663163 33.575851400157113,-94.204265191768783 33.575005164570705,-94.207404999999994 33.574353000000002,-94.209665 33.573509999999999,-94.211329000000006 33.573774,-94.216140999999993 33.576391999999998,-94.217408000000006 33.579259999999998,-94.217197999999982 33.580736999999999,-94.214431000000005 33.583187000000002,-94.212997 33.583486999999991,-94.210966999999997 33.583143,-94.205788416261612 33.581380140341984,-94.203588205048902 33.580815984742067,-94.199751933850294 33.581098061448763,-94.196536237091394 33.581718635888464,-94.194464999999994 33.582886000000002,-94.190890999999979 33.587474,-94.183913000000004 33.594681999999999,-94.180879999999988 33.592612000000003,-94.176327 33.591076999999999,-94.162266000000002 33.588906,-94.161081999999993 33.587972,-94.162009999999995 33.580877,-94.161276999999998 33.579270999999999,-94.156782000000007 33.575749000000002,-94.152625999999998 33.575923000000003,-94.148731999999995 33.580196999999998,-94.146047999999993 33.581975,-94.144383000000005 33.582097999999995,-94.142160000000004 33.581389999999999,-94.141852 33.579590000000003,-94.143023999999983 33.577725,-94.145668999999984 33.575599999999994,-94.149506000000002 33.573602,-94.151257 33.571793,-94.151754999999994 33.569476000000002,-94.151455999999996 33.568387,-94.148520000000005 33.565677999999991,-94.145239000000004 33.564987000000002,-94.143401999999995 33.565504999999995,-94.136863999999989 33.570999999999998,-94.136045999999993 33.571387999999999,-94.135142000000002 33.571033,-94.134308000000004 33.569209,-94.133047999999988 33.557952999999998,-94.131382000000002 33.552934,-94.128658 33.550952000000002,-94.126897999999983 33.550646999999991,-94.123897999999997 33.552100000000003,-94.122878999999983 33.553111999999999,-94.12071899999998 33.560555,-94.120354999999989 33.5655,-94.119902152897509 33.566998927274319,-94.112842999999998 33.566991000000002,-94.103176000000005 33.570349999999998,-94.100106999999994 33.57256799999999,-94.097439999999992 33.573718999999997,-94.088943 33.575322,-94.082640999999995 33.575491999999997,-94.072670000000002 33.572234000000002,-94.072231491265512 33.572605318678747,-94.072031926011121 33.573523317998088,-94.072031926011121 33.573846369634623,-94.072031926011121 33.574161925883949,-94.071815412538413 33.574459631515609,-94.071712621294751 33.574600969288895,-94.071353404455635 33.574840447439463,-94.070395493400298 33.574561056392717,-94.069534727631535 33.574169799087244,-94.069517406590393 33.574161925883949,-94.068559493988133 33.573563230894273,-94.068280102941401 33.571966710019424,-94.06782332144887 33.570215714974232,-94.067801146640278 33.570130711574109,-94.066845999999998 33.568908999999998,-94.061283000000003 33.568804999999998,-94.056597999999994 33.567824999999999,-94.056095999999997 33.567252000000003,-94.055662999999996 33.561886999999999,-94.056442000000004 33.560997999999991,-94.059849999999997 33.559249,-94.061179999999993 33.559159,-94.066685000000007 33.560953999999995,-94.067984999999993 33.560960999999999,-94.071719999999999 33.559682000000002,-94.073744000000005 33.558284999999998,-94.073825999999997 33.555833999999997,-94.072156000000007 33.553863999999997,-94.069091999999998 33.553406000000003,-94.06547999999998 33.550908999999997,-94.061896000000004 33.549764000000003,-94.056095999999997 33.550725999999997,-94.051882000000006 33.552585,-94.050211999999988 33.551082999999998,-94.046040000000005 33.551321000000002,-94.043449999999993 33.552253,-94.043428000000006 33.551424999999995,-94.043374999999997 33.542315000000002,-94.043020107474533 33.494534442391668,-94.043008999999998 33.493039000000003,-94.043278999999998 33.491029999999995,-94.043271947583932 33.489425304099555,-94.043188 33.470323999999991,-94.043130608695648 33.460424000000003,-94.043089437732434 33.453322008844353,-94.043061045958495 33.448424427841935,-94.043010021810147 33.439622762252007,-94.042987999999994 33.435823999999997,-94.042987999999994 33.434442436873745,-94.042987999999994 33.431023999999994,-94.042886999999993 33.420225000000002,-94.042890126641851 33.419424334827802,-94.042891364631132 33.419107312620362,-94.042967439944888 33.399626074590046,-94.043053 33.377715999999999,-94.042868999999996 33.371169999999999,-94.043127999999996 33.358756999999997,-94.043066999999979 33.352097,-94.043066999999979 33.347351000000003,-94.043066999999979 33.339614667914582,-94.043066999999979 33.330497999999999,-94.042990000000003 33.271227000000003,-94.043049999999994 33.260903999999996,-94.043003999999996 33.250127999999997,-94.042730000000006 33.241822999999997,-94.042876000000007 33.215218999999998,-94.042891999999995 33.202666,-94.042874999999995 33.199784999999999,-94.042718999999991 33.160290999999994,-94.043184999999994 33.143476,-94.043076999999982 33.138162,-94.043007000000003 33.13389,-94.042951563372725 33.117233519058104,-94.042869999999994 33.092726999999996,-94.043036 33.079484999999998,-94.042963999999998 33.019219,-94.042986850316112 33.007494023677346,-94.043068075862834 32.965815492539427,-94.043087999999997 32.955592000000003,-94.043066999999979 32.937902999999999,-94.043092 32.910021,-94.042884999999998 32.898910999999998,-94.042859000000007 32.892771000000003,-94.042885999999996 32.880965000000003,-94.043025 32.880445999999999,-94.042784999999995 32.871485999999997,-94.042921087938623 32.829694015190334,-94.043025999999983 32.797476000000003,-94.042747000000006 32.786973000000003,-94.042828999999998 32.785277,-94.042937999999992 32.780557999999999,-94.043026999999995 32.776862999999999,-94.042946999999998 32.767991000000002,-94.042974715046569 32.757603400545236,-94.04314699999999 32.693030999999998,-94.042912999999999 32.655126999999993,-94.042779999999993 32.643465999999997,-94.042823999999996 32.640304999999998,-94.042925999999994 32.622014999999998,-94.042929 32.618259999999992,-94.042918999999998 32.610142000000003,-94.042939007208048 32.604544739553475,-94.043082999999996 32.564261000000002,-94.043142000000003 32.559502000000002,-94.043081 32.513612999999999,-94.042884999999998 32.505144999999999,-94.042911000000004 32.492851999999999,-94.043088999999995 32.486561000000002,-94.043071999999995 32.48429999999999,-94.042955000000006 32.480260999999999,-94.042995000000005 32.478003999999999,-94.042901999999998 32.472905999999995,-94.042874999999995 32.471347999999992,-94.042902999999981 32.470385999999998,-94.042907999999997 32.439890999999996,-94.042985999999999 32.435507,-94.042898999999991 32.400658999999997,-94.042923000000002 32.399918,-94.042900999999986 32.392282999999999,-94.042762999999994 32.373331999999998,-94.042738999999997 32.363559000000002,-94.042733519218956 32.277818574933548,-94.042732999999998 32.269696000000003,-94.042732 32.269620000000003,-94.042671451745775 32.225096273752321,-94.042662000000007 32.218145999999997,-94.042600205901905 32.185155675879351,-94.042565999999994 32.166893999999999,-94.042538999999991 32.166826,-94.042591000000002 32.158096999999998,-94.042681000000002 32.137956000000003,-94.042751999999993 32.125163,-94.042337000000003 32.119914,-94.042699999999996 32.056012000000003,-94.042717288248511 32.00695918811028,-94.042720000000003 31.999265,-94.042490448495869 31.997488887291052,-94.042251674184627 31.995641414801661,-94.041832999999997 31.992401999999998,-94.038411999999994 31.992436999999999,-94.029282999999992 31.995864999999998,-94.027080554152732 31.994823406342874,-94.018664 31.990842999999998,-94.011671046736083 31.979908989829021,-94.008352361284565 31.974719974671689,-94.002944198469422 31.966263903968002,-93.995504541226836 31.954631438414648,-93.994147015257326 31.952508844111783,-93.977461000000005 31.926418999999996,-93.971711999999982 31.920383999999995,-93.953546000000003 31.910562999999996,-93.943541310721315 31.908563758569336,-93.938002035663573 31.906916949339585,-93.935007833635339 31.903773037645127,-93.932462763507019 31.895538979891644,-93.931327794714704 31.894581351390723,-93.92767203678045 31.891496810199779,-93.923929283519882 31.889849995167665,-93.919587694495533 31.890748256066246,-93.915948999999998 31.892861000000003,-93.909557114944889 31.893143619429534,-93.905252294448232 31.890856686636408,-93.90476638821832 31.890598549301192,-93.901173350426333 31.885957535142037,-93.901888 31.880063,-93.898135577976262 31.874953416991548,-93.896981462364707 31.873381885463036,-93.889196542313442 31.867692899288475,-93.888241004857136 31.85786451213389,-93.888148571748644 31.856913771406646,-93.887306164689562 31.854968889155742,-93.884117000000003 31.847605999999995,-93.880377455095243 31.844791786271248,-93.879654915472827 31.84424803536659,-93.874821999999995 31.840611000000003,-93.874804231839249 31.835091218914506,-93.874787826198073 31.829994712349503,-93.874761000000007 31.821660999999999,-93.870917000000006 31.816836999999996,-93.868473098390325 31.815251608244314,-93.853390000000005 31.805467,-93.846187999999998 31.802021,-93.839950728453459 31.798597132180646,-93.836868453653821 31.794158659336233,-93.836868453653821 31.791454071635616,-93.836868453653821 31.788733862378688,-93.834649214842401 31.783309060642711,-93.831197070889573 31.780226788232302,-93.827450999999996 31.777740999999999,-93.823442999999997 31.775098,-93.822597999999985 31.773558999999995,-93.826519525540022 31.761832440276638,-93.827342999999999 31.759369999999997,-93.830112066651125 31.754555168030393,-93.830647423185951 31.745811043570992,-93.824579 31.734396999999998,-93.819048075401312 31.72885814427071,-93.818932598107111 31.728523867973351,-93.815657495541259 31.719043310199801,-93.815835943108667 31.711905251886723,-93.815525624652665 31.710796971318612,-93.814600367558683 31.707492480599399,-93.814586782471608 31.707443962415162,-93.811060073548262 31.705827553684028,-93.810303944025605 31.705480994217723,-93.807270273132957 31.704231833580664,-93.806045429297654 31.703104120446881,-93.803419352361757 31.700686292667093,-93.802693549340276 31.697783082925291,-93.802451615781152 31.693186330065117,-93.804479 31.685663999999999,-93.812820813246589 31.676953615984285,-93.81562111199878 31.674029590143711,-93.817425 31.672146,-93.821829497696953 31.673879806810671,-93.822051000000002 31.673966999999998,-93.822341750589956 31.673502431839047,-93.826462000000006 31.666919,-93.8257324849111 31.66154827530681,-93.825660999999997 31.661021999999996,-93.825228912080746 31.660277861177896,-93.81803699999999 31.647891999999999,-93.817707248836442 31.6409111211136,-93.816838000000004 31.622509,-93.818717000000007 31.614555999999997,-93.823976999999999 31.614227999999997,-93.825414416100287 31.615089707767993,-93.827851999999993 31.616550999999998,-93.838056999999992 31.606795000000002,-93.839382999999998 31.599074999999999,-93.837534908858984 31.593743346167731,-93.834924 31.586210999999999,-93.822958 31.568129999999996,-93.822219025006859 31.564792487143556,-93.820763999999997 31.558221000000003,-93.818582000000006 31.554825999999998,-93.798086999999995 31.534044000000002,-93.787687000000005 31.527343999999996,-93.781574079702807 31.525595412174177,-93.780834999999996 31.525383999999999,-93.777170583402579 31.525128039451072,-93.760062000000005 31.523933,-93.753860000000003 31.525331,-93.751899169046524 31.525601857922521,-93.749869902733366 31.526210635456998,-93.746826003263621 31.526007705679731,-93.743376259969125 31.525196002300419,-93.742401241878753 31.523787647735123,-93.74154991556837 31.522557958452786,-93.741111000000004 31.520101,-93.740752889151466 31.518711098615515,-93.740332360499409 31.517078940980245,-93.739317727342822 31.515049678599539,-93.734411826534469 31.513527159467717,-93.733432858180635 31.513223342063657,-93.726736285639134 31.511599931372594,-93.725924582259807 31.504091651519342,-93.728765551952293 31.496786301443361,-93.730997740177827 31.492118989316349,-93.733996137544537 31.48847587643743,-93.737167999999997 31.484621999999998,-93.741884999999982 31.483535,-93.745608448194673 31.481972669547908,-93.746355058504165 31.481633300507966,-93.747840636420193 31.480958036391328,-93.749869902733366 31.478928770078173,-93.749869902733366 31.475276095040186,-93.749626709600278 31.47120988033301,-93.749476 31.468689999999999,-93.709416000000004 31.442995,-93.706857276389258 31.44142369846492,-93.700929751125955 31.437783629836048,-93.697919763270235 31.429300939077926,-93.697603150134782 31.428408665937056,-93.704678 31.418899999999997,-93.704697874245824 31.418107106580852,-93.704878999999991 31.410881,-93.701611 31.409333999999998,-93.695865999999995 31.409391999999997,-93.689953513429003 31.406208353384852,-93.678362516190631 31.399967047179565,-93.675064666986117 31.398191282223291,-93.674659331220113 31.397973024503138,-93.674116999999981 31.397680999999995,-93.671643999999986 31.393352,-93.670181999999983 31.387184,-93.668532759396427 31.379357124595749,-93.668145986696857 31.37510269235548,-93.669064113561134 31.373151679996884,-93.669693055010129 31.371815184369147,-93.669512094168894 31.370548454097019,-93.668919524600994 31.366400452767671,-93.667402247962755 31.365414223393856,-93.665051865060306 31.363886475190469,-93.663891561951601 31.361952641672623,-93.663698175601809 31.360018811902275,-93.664665092360735 31.357698213179859,-93.665825387974422 31.355184231855155,-93.668439000000006 31.353011999999996,-93.669515978941746 31.350266666374935,-93.673736148309771 31.339509006758242,-93.677277000000004 31.330482999999997,-93.687850999999995 31.309835,-93.686922292404276 31.305369360695714,-93.686880000000002 31.305166,-93.686723586265984 31.304979085312567,-93.684737187533131 31.30260533533086,-93.684038999999999 31.301770999999995,-93.683824154244718 31.301752735987076,-93.675439999999995 31.30104,-93.668927999999994 31.297974999999997,-93.657003999999986 31.281735999999999,-93.647719584117951 31.27389987096868,-93.642516 31.269508000000002,-93.632650200244299 31.270182983909681,-93.620343000000005 31.271025000000002,-93.615257056394739 31.261768439618621,-93.61394199999998 31.259374999999995,-93.614287999999988 31.251631,-93.616308000000004 31.244595000000004,-93.616007481020262 31.233959925788763,-93.613835693061347 31.232449117569455,-93.609977782626956 31.229765355202261,-93.609827614285464 31.229660890324059,-93.608033931702224 31.227867209671889,-93.60740940488401 31.227242683526022,-93.605259887151632 31.224152751460338,-93.604319472818304 31.220794125122111,-93.604319472818304 31.215285983654958,-93.607287999999997 31.205403,-93.602442999999994 31.182541,-93.600307999999998 31.176157999999997,-93.598827999999997 31.174679,-93.595531708436056 31.171774432382691,-93.588772842417583 31.16581877494577,-93.588502999999989 31.165581,-93.588046655571759 31.165671453282986,-93.583339301771971 31.166604510813709,-93.579215000000005 31.167421999999995,-93.578993496784307 31.167654977688123,-93.574136071231791 31.172764031170193,-93.569563000000002 31.177574,-93.560942999999995 31.182481999999997,-93.552649000000002 31.185575,-93.548930999999996 31.186601,-93.535096999999993 31.185614,-93.533756450961803 31.184752004501149,-93.533306999999994 31.184463,-93.5330935917342 31.183965183917415,-93.531744000000003 31.180816999999998,-93.533193520062923 31.174490811082034,-93.53417786817559 31.170194787673282,-93.536829999999981 31.158620000000003,-93.540253000000007 31.156578999999997,-93.544009577425854 31.153014849431809,-93.544887639546317 31.148844041597808,-93.544887639546317 31.143136612291205,-93.544701999999987 31.135888999999999,-93.540277800652078 31.128868068802213,-93.539619249807799 31.121843550568837,-93.541375382556595 31.113501939154769,-93.541635919885238 31.113241401693255,-93.548470239502265 31.106407078590973,-93.549716998224596 31.105160319232844,-93.55112206776522 31.099540038044921,-93.551692642249563 31.097257738879012,-93.551034091405285 31.091111287020031,-93.546643772295099 31.082989189009115,-93.540128999999993 31.078002999999995,-93.53104031045666 31.074698717981779,-93.527873992025093 31.072210897922254,-93.526043656150705 31.070772777782928,-93.524020490083288 31.067083472240864,-93.523009982318626 31.065240780256033,-93.523659621286654 31.063941504256789,-93.525329858273139 31.060601035263321,-93.529255791555698 31.057567354514948,-93.532069000000007 31.055264,-93.531218761655126 31.051678447674814,-93.523247999999995 31.037841999999998,-93.516942620821894 31.032584114108545,-93.516407263768343 31.029550433360171,-93.516883288197775 31.024314186160638,-93.516942620821894 31.023661529978199,-93.539525999999995 31.008497999999999,-93.540062152267268 31.008345085566372,-93.540618575989114 31.008186389569964,-93.555580999999989 31.003918999999996,-93.562626264226125 31.00599480945781,-93.566016847371429 31.004567194682853,-93.567979815741779 31.001533515663564,-93.569764334642755 30.996715319472376,-93.571101253513504 30.991033414001794,-93.571905755076102 30.987614282198351,-93.567971999999997 30.977981,-93.560533000000007 30.971285999999999,-93.55046299120518 30.967360467203815,-93.549841 30.967117999999996,-93.539153617393822 30.956968324013513,-93.532549000000003 30.950695999999997,-93.526524876707626 30.939912016599852,-93.526293050708773 30.939497017171423,-93.526245000000003 30.939411,-93.526242458076936 30.939167805401102,-93.526231146824912 30.938085618677029,-93.526146999999995 30.930035,-93.526269219450654 30.929894609689271,-93.530935999999983 30.924533999999998,-93.542488999999989 30.920064,-93.54502991280485 30.920837107400992,-93.546884259495414 30.92151141825828,-93.549244331161987 30.921005686748707,-93.550358562122057 30.920030731622223,-93.551941554990407 30.918645608548566,-93.555650241837967 30.911228247920597,-93.555751501305451 30.910053639118178,-93.555774257662236 30.909789665608852,-93.556493125509377 30.9014508058257,-93.562447641167196 30.896531852982324,-93.563812214002311 30.895404595987301,-93.564247644832776 30.895044891882932,-93.567787752332634 30.888301832311882,-93.567450600170787 30.878524390216981,-93.566008182600839 30.875519355165832,-93.565853452041281 30.875197,-93.565427680666076 30.874309976760035,-93.563763247930751 30.87311591407202,-93.559394659034922 30.869981891957959,-93.55904186947815 30.86972880101742,-93.558616999999984 30.869423999999999,-93.558608112367665 30.868835818489011,-93.558593354020289 30.867859114376206,-93.558393833586933 30.854654896933653,-93.558352334289978 30.851908482785802,-93.558231866260058 30.843935935637496,-93.558171999999999 30.839973999999998,-93.553625999999994 30.835139999999999,-93.55374115920948 30.832414921630001,-93.554057 30.824940999999995,-93.561666000000002 30.807738999999998,-93.563243 30.806218000000005,-93.564501487304341 30.805543276361082,-93.569303000000005 30.802969,-93.578395 30.802046999999998,-93.584264999999988 30.796662999999995,-93.588934854633479 30.787551489258888,-93.589380999999989 30.786681000000002,-93.589895999999996 30.77776,-93.591925627378814 30.768225181611253,-93.592827999999997 30.763985999999996,-93.607757000000007 30.757656999999995,-93.611581311334461 30.752392350515215,-93.615058988962588 30.747604886281291,-93.619129 30.742001999999996,-93.617688 30.738479000000005,-93.609908791486006 30.729403419430973,-93.609718999999998 30.729181999999998,-93.609544 30.723138999999996,-93.61030547238029 30.720788970554505,-93.611192000000003 30.718053,-93.61618399999999 30.713980000000003,-93.616977218405893 30.712276394979256,-93.620773999999997 30.704122000000005,-93.621061387132315 30.696047232392139,-93.621092999999988 30.695159,-93.62235785978659 30.692974242186811,-93.629903999999996 30.679939999999995,-93.632922525899005 30.677439880221801,-93.638212999999993 30.673057999999997,-93.646373493696458 30.671658473870171,-93.653439445062318 30.670446661945991,-93.654970999999989 30.670183999999999,-93.666219386787546 30.661299653678171,-93.670353999999989 30.658033999999997,-93.670860468306827 30.657347728689221,-93.683099999999996 30.640763000000003,-93.685120999999981 30.625201,-93.684323003112922 30.617258061147268,-93.683396999999985 30.608040999999997,-93.680812614601606 30.602993111696524,-93.680648411741274 30.602453589051585,-93.679828117977763 30.599758343304948,-93.681234543283509 30.596101640780542,-93.683902548646543 30.593069810094963,-93.684328672415049 30.59258557751615,-93.684347894395486 30.592579170189346,-93.687282162286593 30.59160108089231,-93.689533999999995 30.592759,-93.692869000000002 30.594382000000003,-93.712453999999994 30.588479,-93.72107859525056 30.580404159651369,-93.727657631584904 30.574244488790981,-93.727844000000005 30.574069999999995,-93.727840097341868 30.573768021870688,-93.72780696123904 30.571204031383882,-93.727747245613443 30.566583382517752,-93.727745999999996 30.566486999999999,-93.725846999999987 30.556978,-93.728764326223569 30.546403128121515,-93.729195000000004 30.544841999999999,-93.736587760607577 30.541316766984647,-93.740252999999996 30.539569,-93.738909526480768 30.537838512460276,-93.732793 30.529960000000003,-93.727721000000003 30.525671000000003,-93.718711305261792 30.520890798500346,-93.714321999999996 30.518561999999996,-93.710116999999997 30.506399999999996,-93.713193644304937 30.50058809182816,-93.716678000000002 30.494005999999995,-93.71365216083376 30.483878530555742,-93.711446941284677 30.476497671106777,-93.710595424186792 30.473647647388944,-93.709703157003446 30.47066123332699,-93.708899372668625 30.467970970942378,-93.705844999999997 30.457747999999999,-93.697828 30.443837999999996,-93.6978 30.440583,-93.698862234241474 30.438260713588438,-93.702220303997905 30.430919206922557,-93.702664999999996 30.429947000000002,-93.722313999999997 30.420729,-93.729486046484141 30.413342592858086,-93.738025291703323 30.404548123662593,-93.738321805525445 30.404242747530638,-93.745333000000002 30.397022,-93.751243378615428 30.396311282781173,-93.751436999999996 30.396287999999998,-93.754787283332149 30.393127406185759,-93.75746720581121 30.390599218098316,-93.757654000000002 30.390422999999995,-93.757872622462301 30.389610210267922,-93.758470934573069 30.387385818798332,-93.75855399999999 30.387077,-93.758091991038413 30.3842338214119,-93.758032188807945 30.383865801664729,-93.757931393201204 30.383245510859378,-93.75589426835937 30.370709152880856,-93.756044740613035 30.365928314513319,-93.75610723110799 30.3639428524199,-93.756352000000007 30.356166000000002,-93.758519945992674 30.350935458285047,-93.760658351649255 30.34577618769989,-93.763244572034736 30.339536487238696,-93.765822 30.333317999999995,-93.764264999999995 30.330222999999997,-93.760690955159149 30.32995156504764,-93.760328 30.329923999999998,-93.747921244232074 30.314935395431327,-93.743830002049762 30.309992764786195,-93.741160171732417 30.306767342150266,-93.738698999999997 30.303793999999996,-93.734966161369201 30.301561360829975,-93.729390287632427 30.298226388348422,-93.724220000000003 30.295133999999997,-93.718684474451791 30.295009979388311,-93.714319430116035 30.294282471059141,-93.71311219174963 30.293184979315004,-93.711118400234781 30.291372437742464,-93.709949692759622 30.289928741213533,-93.708644873043468 30.288316906143507,-93.708448001046747 30.287627854154977,-93.707590519980414 30.284626670422831,-93.706607851977495 30.281187332412603,-93.706635817233519 30.280914670488997,-93.707189856385114 30.275512775340033,-93.709131999999997 30.271826999999995,-93.707538803456245 30.253087036355307,-93.707271000000006 30.249936999999996,-93.705637767078656 30.244573755694763,-93.705083000000002 30.242751999999996,-93.707646217538425 30.23733474070027,-93.713358999999997 30.225261,-93.71802168481895 30.219915738218987,-93.719219999999993 30.218541999999999,-93.720945999999998 30.209852,-93.717397000000005 30.193439,-93.710467999999992 30.180670999999997,-93.706634735424259 30.17682001000631,-93.705927274531248 30.176109277739844,-93.705791510460429 30.175972885881706,-93.703764000000007 30.173936,-93.703646997347349 30.173527735424756,-93.702964616901454 30.171146663230584,-93.701744610591902 30.166889619937699,-93.701686103512344 30.166685467574972,-93.697748000000004 30.152943999999998,-93.696083741705934 30.150925109485563,-93.688211999999979 30.141376,-93.69286799999999 30.135216999999997,-93.69498 30.135185,-93.698276000000007 30.138608000000005,-93.700984936503929 30.137486558544058,-93.701251999999997 30.137376,-93.7012922571502 30.136537706048752,-93.701585086407604 30.130439981942867,-93.701656556414648 30.128951727699913,-93.701742498922442 30.127162105630983,-93.701985639262489 30.122099077688652,-93.702366286953335 30.114172668214064,-93.702403861450691 30.11339023643033,-93.702436000000006 30.112720999999997,-93.70268530105453 30.112503701678254,-93.704703872918159 30.110744253531749,-93.710410303455276 30.105770356484715,-93.714491639657382 30.10221294069715,-93.723764999999986 30.094129999999996,-93.727140999999989 30.092110594495406,-93.7293848599641 30.090768395691203,-93.72996305770063 30.09042253796256,-93.732484999999997 30.088913999999995,-93.734084999999979 30.086129999999997,-93.731705540540531 30.081478540540548,-93.731605000000002 30.081282000000002,-93.729179922109864 30.079341937687897,-93.727017487667368 30.077611990133899,-93.71640499999998 30.069121999999997,-93.716151269697463 30.069056930886212,-93.707507094464489 30.066840132907302,-93.702179999999998 30.065473999999995,-93.700580000000002 30.063666,-93.699479012508235 30.0595596142199,-93.699395999999993 30.059249999999995,-93.699786698153744 30.05843348475733,-93.700658293446523 30.056611948527472,-93.700819999999993 30.056273999999998,-93.702099264262131 30.055460929156467,-93.703267223154896 30.054718601437116,-93.703940000000003 30.054290999999999,-93.704473210548429 30.054251542735575,-93.709782747672762 30.053858640136632,-93.710785394684464 30.05378444485228,-93.720804999999999 30.053042999999995,-93.729054152595779 30.045230570163486,-93.729990027439044 30.044344241966268,-93.737446000000006 30.037282999999999,-93.739158000000003 30.032626999999998,-93.739733999999999 30.023987000000002,-93.741078000000002 30.021571000000002,-93.744068611359822 30.019549890100706,-93.74834924745744 30.01665695787004,-93.753252045558341 30.013343557169065,-93.766227014667976 30.004574835541469,-93.782835629207284 29.993350429819579,-93.786934999999986 29.990579999999998,-93.789430999999993 29.987812000000002,-93.803328792953494 29.962666096659486,-93.807814999999991 29.954549,-93.813734999999994 29.935126,-93.816550000000007 29.920725999999995,-93.818997999999993 29.914822,-93.830374000000006 29.894358999999998,-93.838374000000002 29.882854999999999,-93.853129483564672 29.866348149842587,-93.854474509531912 29.864843479256791,-93.855140000000006 29.864098999999996,-93.857517787207271 29.862146563102172,-93.862474802662518 29.858076283033213,-93.863569999999996 29.857177,-93.864820388168837 29.856398395289631,-93.872445999999997 29.851650000000003,-93.890679000000006 29.843159000000004,-93.900728 29.836967,-93.911111176241661 29.828996955749506,-93.916359999999997 29.824967999999998,-93.922743999999994 29.818808,-93.927992000000003 29.809640000000002,-93.929208000000003 29.802952,-93.928808000000004 29.79708,-93.926503999999994 29.789559999999998,-93.922407000000007 29.785048,-93.898470000000003 29.771576999999997,-93.893861999999999 29.767289000000002,-93.890820999999988 29.761672999999995,-93.891780610698319 29.758916671398389,-93.892526767476923 29.756773455119472,-93.893828999999997 29.753032999999999,-93.891732576672823 29.744984915009951,-93.891637000000003 29.744618000000003,-93.891484462723341 29.744488863328282,-93.888820999999993 29.742234000000003,-93.873941000000002 29.737770000000005,-93.870019999999997 29.735482,-93.863203999999996 29.724059,-93.837970999999982 29.690618999999998,-93.852868 29.675885,-93.866980999999996 29.673085,-93.889989999999997 29.674012999999999,-93.930999999999997 29.679611999999999,-93.955443390383635 29.680262610936268,-93.955453232202885 29.68026287289646,-93.991585827226885 29.681224615973395,-94.000169999999997 29.681453101326589,-94.000222586816207 29.681454501032487,-94.001406000000003 29.681486,-94.010062765207763 29.679864152681674,-94.056505999999999 29.671163,-94.132576999999984 29.646217,-94.354166823921716 29.561457673765378,-94.370794193562546 29.555097613439198,-94.391123278008692 29.54732162684321,-94.45341877657637 29.523493256060661,-94.456196753642018 29.522430664556182,-94.45805240375158 29.521720868184531,-94.499046371673685 29.506040450016997,-94.499089575343746 29.506023924375612,-94.500455432020999 29.505501476685335,-94.500806999999995 29.505367,-94.552043946167103 29.48495633977835,-94.552044379602123 29.484956167115939,-94.568074121423052 29.478570587212708,-94.593696728539513 29.46836361027578,-94.594852999999986 29.467903,-94.599458690353401 29.465813271763974,-94.62931993394271 29.452264405230761,-94.631084 29.451464,-94.634656044789836 29.449584234717392,-94.661528069434993 29.435443006940758,-94.670389 29.430779999999999,-94.674924987623086 29.427889211977174,-94.680871144138621 29.42409972235215,-94.694158000000002 29.415631999999999,-94.708472999999998 29.403049000000003,-94.723958999999979 29.383268000000005,-94.730956033155138 29.369322304827527,-94.731047000000004 29.369140999999996,-94.731324722869132 29.369141342444962,-94.731537324603394 29.369141604592603,-94.742750849251038 29.369155431380086,-94.744595216508316 29.369157705569055,-94.744833999999997 29.369157999999999,-94.761491000000007 29.361882999999999,-94.772184717669788 29.3616343088914,-94.778690999999995 29.361483,-94.780073439975141 29.362532749099824,-94.782355999999993 29.364266,-94.782645420567292 29.368514320481975,-94.783130999999997 29.375641999999996,-94.766847999999996 29.393488999999999,-94.754099999999994 29.400999999999996,-94.743385419572206 29.410035318862811,-94.73704402931665 29.415382843516582,-94.727822669267709 29.423158969605012,-94.723817999999994 29.426535999999995,-94.716270519001597 29.430976788539081,-94.706539419927282 29.436702374764607,-94.706364999999991 29.436804999999996,-94.686385999999999 29.466508999999995,-94.681540999999996 29.471388999999999,-94.672399999999996 29.476842999999999,-94.665852999999998 29.478401000000002,-94.656737000000007 29.478032999999996,-94.645948000000004 29.473769,-94.628217000000006 29.475986000000002,-94.608557000000005 29.483344999999996,-94.594211 29.492127,-94.595122262498009 29.503650874486677,-94.595439999999996 29.507669,-94.591407000000004 29.513857999999999,-94.580274000000003 29.525295,-94.566674000000006 29.531987999999995,-94.553989999999985 29.529558999999999,-94.546993999999998 29.524379,-94.532347999999999 29.5178,-94.511044999999996 29.519649999999995,-94.495024999999984 29.525030999999998,-94.503428999999997 29.543249999999997,-94.509486999999993 29.542589999999997,-94.523742999999996 29.545987,-94.523871183237745 29.546315590042923,-94.5261306113389 29.55210749848424,-94.526336 29.552633999999998,-94.542531999999994 29.568999999999999,-94.546193009360195 29.571896121601313,-94.546385 29.572047999999995,-94.546803832691651 29.57214903106096,-94.55398799999999 29.573882,-94.570006000000006 29.572232,-94.578210999999982 29.567281,-94.593518000000003 29.561319,-94.625889999999998 29.552807999999999,-94.634988842169335 29.550728838088908,-94.643914431984982 29.54868926579681,-94.666855093063802 29.543447131924982,-94.691625000000002 29.537787000000002,-94.693243673136905 29.537529479678042,-94.718275999999989 29.533546999999995,-94.740699000000006 29.525857999999999,-94.757688999999999 29.524616999999999,-94.768675999999999 29.525659,-94.780938000000006 29.531093000000002,-94.785987568065153 29.540133852805589,-94.789123822994313 29.545749069554745,-94.789562096440733 29.546533763545689,-94.78971899677407 29.546814681200559,-94.790604999999999 29.548400999999998,-94.779438999999996 29.549472000000002,-94.772471009935998 29.548613672580952,-94.771052999999995 29.548438999999995,-94.762972090755014 29.555767305595641,-94.75523699999998 29.562781999999999,-94.750081379755628 29.56810096117977,-94.734626000000006 29.584046,-94.731874420576986 29.588423440241069,-94.724443803393115 29.600244680945409,-94.708741000000003 29.625226,-94.705273448792255 29.640626536822896,-94.703938528752261 29.6465553563269,-94.702680930363101 29.65214076491651,-94.702542126991759 29.652757236398369,-94.699660909360375 29.665553672721437,-94.69780371146922 29.673802100155214,-94.694887994254785 29.686751760487848,-94.693153999999993 29.694452999999999,-94.692434000000006 29.70361,-94.692611750388934 29.704808689927841,-94.695098387970873 29.721577752663908,-94.695317000000003 29.723051999999999,-94.695611486157915 29.72357178078331,-94.697558868014866 29.727008993840069,-94.705700105090941 29.741378628781629,-94.713878412983604 29.755813695314995,-94.714586470042065 29.757063446593907,-94.722078339612551 29.770286919851305,-94.724615999999983 29.774766,-94.735270999999997 29.785433,-94.738125273271791 29.786265833277604,-94.740919000000005 29.787080999999997,-94.749144589762409 29.783534045463153,-94.75591801041729 29.780613280409739,-94.771512 29.773888999999997,-94.792237999999998 29.767432999999997,-94.798897371247961 29.764438558858895,-94.816085 29.756710000000002,-94.81943931055217 29.753325616252695,-94.823987131664666 29.748737021482022,-94.851107999999996 29.721373000000003,-94.856932183541645 29.710462974624775,-94.860426638443812 29.703917062844585,-94.864167858487249 29.696908903620852,-94.865007000000006 29.695336999999999,-94.865123196353878 29.694545038919138,-94.867438000000007 29.678768000000002,-94.872550999999987 29.67125,-94.893107 29.661335999999999,-94.915413 29.656613999999998,-94.921317999999999 29.658177999999999,-94.928410408640929 29.669495826038883,-94.930071799099807 29.672147016790593,-94.930110565443542 29.672208878811933,-94.930656833967475 29.673080595662611,-94.931474856161643 29.67438596783704,-94.934166999999988 29.678681999999998,-94.935264231405057 29.686686879688732,-94.935319060033862 29.687086883348073,-94.935997030967087 29.692033037575822,-94.936088999999996 29.692703999999999,-94.936280063994801 29.692851065945039,-94.941277009519624 29.696697319220664,-94.942680999999993 29.697778,-94.942922907078852 29.697804516058127,-94.95311095436017 29.698921254167477,-94.965343618259496 29.700262107971746,-94.965962999999988 29.70033,-94.972666000000004 29.684869999999997,-94.980123280315652 29.679059463608382,-94.986438191421769 29.674139034277729,-94.988580124776774 29.672470090483102,-95.001800051879343 29.662169436052462,-95.002396227716986 29.661704909944582,-95.005398 29.659365999999995,-95.011683000000005 29.649802000000005,-95.013860566469219 29.644103309100927,-95.014229369455222 29.643138151779844,-95.015636 29.639457,-95.015582911847446 29.639202042718885,-95.014543866006605 29.634211997110764,-95.013498999999996 29.629193999999998,-95.006633444897872 29.623869765921089,-95.00056235200779 29.61916163683599,-95.000370188745762 29.619012614335521,-94.997782627529062 29.617005962082896,-94.997731096758997 29.616965999999998,-94.995478872439179 29.615219401320278,-94.993499353954022 29.613684285860323,-94.988871000000003 29.610095,-94.988045541621688 29.608923290953985,-94.982835617337045 29.60152798723708,-94.982705999999993 29.601344000000005,-94.982886347998431 29.601051719777942,-94.983895794498991 29.599415764569699,-94.98693593300419 29.594488776939745,-94.988992999999994 29.591155,-94.991605757589539 29.588791109774153,-94.991811610459919 29.588604864563266,-94.992208959522046 29.588245363334387,-95.006677600766466 29.575154872369662,-95.007235451984087 29.574650156950938,-95.007670000000005 29.574256999999996,-95.00815781845921 29.573398130166158,-95.016627 29.558487,-95.018253 29.554884999999999,-95.016926355758628 29.548485488141377,-95.015164999999996 29.539988999999998,-95.012091452887788 29.536262245236642,-95.011587670186657 29.535651395780732,-95.011086587278399 29.535043819893005,-95.001666768519442 29.523622047865974,-94.999580999999992 29.521093,-94.989064037839412 29.515168017977793,-94.982063906682782 29.511224326765198,-94.981915999999998 29.511140999999995,-94.981645984265953 29.511070508097891,-94.961088181447877 29.505703566689924,-94.958443000000003 29.505013000000002,-94.958183821928699 29.504969740154092,-94.957844913785792 29.504913172428417,-94.95747910332102 29.504852114391142,-94.955724072517796 29.504559179260749,-94.952845264169682 29.504078672538427,-94.930551536860648 29.500357589179547,-94.927405495678158 29.499832478177321,-94.909464999999997 29.496837999999997,-94.913072085311995 29.488019044482122,-94.913385000000005 29.487254,-94.917178632618899 29.481741136316387,-94.925104227340569 29.470223752399235,-94.925293406029382 29.469948840084836,-94.925914000000006 29.469047,-94.930860999999993 29.450503999999999,-94.923011455799639 29.448810114938265,-94.920334997737442 29.448232551169699,-94.919400999999993 29.448030999999997,-94.916063708203708 29.446327523795176,-94.890799999999984 29.433432,-94.888257132054065 29.420136433311271,-94.887299999999996 29.415132,-94.887087039327696 29.40154064293051,-94.886938304445962 29.392048238229908,-94.886925408340758 29.391225196262944,-94.886904215833582 29.389872669858736,-94.886764190565785 29.380936121184895,-94.886591910643261 29.369941049100753,-94.886536208040312 29.366386054846032,-94.886982892003275 29.364738908620176,-94.888420210858001 29.359438798199445,-94.888544709543254 29.358979709544975,-94.888781730376067 29.358105695694917,-94.894234145274325 29.337999926591962,-94.894147383920654 29.327241695272729,-94.894002695197727 29.309300588032027,-94.893993580875261 29.308170430590454,-94.891329624021282 29.304475264201969,-94.888683946869179 29.30080545353194,-94.886599269217157 29.297913803549264,-94.886536208040312 29.297826331584119,-94.885816422022174 29.297499155955627,-94.884216982863776 29.296772137788121,-94.876917125093229 29.293454018939102,-94.875951551627523 29.293015121686942,-94.86617837453683 29.293883848703121,-94.865126326153927 29.293977364132552,-94.861112574100105 29.294855372432302,-94.849730461009372 29.297345209778594,-94.825607939204673 29.30577638202961,-94.824952733476934 29.30600538596191,-94.823862547308252 29.313200608971236,-94.822547126780194 29.321882377573708,-94.82230657170463 29.344254498409398,-94.810695999999993 29.353434999999998,-94.797913847039041 29.344567105809805,-94.79693012269928 29.343884625840758,-94.784895000000006 29.335535,-94.779995 29.334935000000002,-94.777063999999996 29.336811,-94.773074869484503 29.336485139838025,-94.745528999999991 29.334235,-94.744945 29.33641,-94.731319999999997 29.338066,-94.722529999999992 29.331445999999996,-94.731082 29.331833,-94.769694999999999 29.304936,-94.780304129101268 29.295750693651897,-94.786095000000003 29.290737,-94.793321795438203 29.286014946162535,-94.795651468635626 29.284492716516493,-94.803695000000005 29.279236999999998,-94.809348860129617 29.275904173901317,-94.81020919937113 29.275397022855465,-94.819018398045671 29.270204194137058,-94.82210781776098 29.268383049216432,-94.825036245866656 29.266656805306088,-94.825782574142963 29.266216861214712,-94.826962003659688 29.265521613475173,-94.833188167130103 29.261851427154124,-94.844390135018429 29.255248113651685,-94.870677905110114 29.23975205180561,-94.881596387366812 29.233315846843187,-94.896165027201874 29.224727954332334,-94.925556066041452 29.207402583865772,-94.927613957028456 29.206189502425392,-94.940693735267132 29.198479261054111,-94.968741214129224 29.181945889621023,-94.978383546115566 29.176261947153485,-94.981700088962569 29.174306918145966,-95.026218999999998 29.148064000000002,-95.076832914261459 29.114498139229923,-95.081772999999998 29.111222,-95.084611040272236 29.108948681405003,-95.100241410561338 29.096428488590096,-95.110484 29.088224,-95.116308293211759 29.081343778536318,-95.119264367911811 29.077851775668329,-95.119484217932538 29.077592067446851,-95.122403192505772 29.074143890856067,-95.122524999999996 29.074000000000002,-95.122638295373392 29.073709965581063,-95.125134000000003 29.067321,-95.183550616106302 29.028323983126334,-95.191390999999996 29.023089999999996,-95.192301227546167 29.02243038040822,-95.237672480263356 28.989550945676651,-95.238923999999997 28.988644,-95.240558404572027 28.987315672512366,-95.251568580535192 28.978367386619187,-95.251619678822578 28.978325857575001,-95.272266000000002 28.961545999999995,-95.29656403895315 28.934716691525271,-95.297146999999981 28.934073,-95.309703999999996 28.928262,-95.334686660244515 28.911063042846731,-95.353450999999993 28.898145,-95.376979000000006 28.876159999999999,-95.377903963555724 28.874482723635413,-95.38239 28.866347999999999,-95.416173999999998 28.859482,-95.436326577581042 28.85908617652915,-95.439594 28.859022000000003,-95.449516932572138 28.854239851149391,-95.480746000949352 28.839189657836059,-95.485144835951402 28.837069731735976,-95.486768999999995 28.836286999999995,-95.506945757563287 28.824808612805594,-95.536465934189138 28.808014833314715,-95.564052739104355 28.79232093268277,-95.564094788066555 28.792297011382839,-95.564132228771385 28.792275711681647,-95.568135999999981 28.789997999999997,-95.576201168007842 28.785870627961152,-95.606319447682353 28.770457515020929,-95.613122366343802 28.766976102576891,-95.695711236705606 28.724711019702028,-95.715243498124195 28.714715330888584,-95.812504000000004 28.664942,-95.854124934570734 28.646410960033691,-95.884026000000006 28.633098,-95.920915435374582 28.618912188118028,-95.97832748198536 28.596834417485056,-96.000681999999983 28.588238,-96.000998496292368 28.588108377001081,-96.024040703743012 28.578671299526803,-96.035336417502748 28.574045070633311,-96.05294532761279 28.566833233429691,-96.077867999999995 28.556625999999998,-96.194412 28.502223999999998,-96.220123346321373 28.492065891861738,-96.220376184174313 28.491966,-96.226882700448613 28.487451845000788,-96.241923733725443 28.477016528665938,-96.244750999999994 28.475055,-96.270391000000004 28.461929999999999,-96.303212000000002 28.441870999999999,-96.321560000000005 28.425148,-96.328817 28.423658999999997,-96.341616999999999 28.417333999999997,-96.371116999999998 28.397660999999999,-96.372100999999986 28.393874999999998,-96.370716999999985 28.387667,-96.378616389014681 28.383909329746462,-96.379349732835649 28.386024690464755,-96.381702691108558 28.392811896356477,-96.381863685354148 28.393276290946375,-96.375880899310658 28.401794149460329,-96.37413840285555 28.404274990018202,-96.340801887331921 28.432913073072363,-96.338559687910475 28.434839257985413,-96.335119195902806 28.437794848703732,-96.312964581561445 28.451131053409146,-96.280819757359396 28.470480970729877,-96.274497619264608 28.474286648703266,-96.268341347214189 28.477992481854848,-96.252027698910211 28.484249764669329,-96.250247000000002 28.484932771712327,-96.223824788704931 28.495067307260509,-96.218978121459415 28.500382701101032,-96.21505000939203 28.509679222336811,-96.145447855080619 28.544740658174199,-96.10473518795402 28.559498996555909,-96.046210731424992 28.586980036018211,-96.032979113622659 28.589015683181284,-96.007533711461477 28.59970275682273,-95.986159544454679 28.60631857558586,-95.982088289576367 28.614461085342473,-95.98565064745685 28.621076904105603,-95.983106103295938 28.641942154390655,-95.97852589224803 28.650593590730978,-95.986065974637228 28.655467849280154,-95.996337701374344 28.658736120211515,-96.002953500413568 28.656191585912577,-96.006515878017979 28.648049056432036,-96.010005951759737 28.648641710656278,-96.010506546843942 28.648726717396318,-96.011440067305529 28.648885239790378,-96.014343010193883 28.649378192516537,-96.026200716522851 28.65139176594381,-96.03348799089656 28.652629228032097,-96.039323368019012 28.651170385770797,-96.047737442142406 28.649066870151618,-96.049244844336243 28.648218956037223,-96.052682972641108 28.646285007998213,-96.05836686193534 28.643087818836001,-96.072165030584017 28.635326345489485,-96.092812363862251 28.627145317384699,-96.098878916233431 28.624741586366319,-96.099137163186526 28.624639261986079,-96.099760206508392 28.62447226081035,-96.102639895829256 28.623700385909448,-96.141413249033207 28.613307536255487,-96.148501276515404 28.611407654045699,-96.187178316202875 28.593595864643298,-96.198374286842139 28.586980055742131,-96.221784081288092 28.580364246840958,-96.228908787187095 28.580873158631725,-96.233997875508891 28.596649310733014,-96.233997875508891 28.601738394123839,-96.222292978285921 28.607336389305434,-96.214150448805384 28.613443281484855,-96.212623728226021 28.62260364440889,-96.2309444244882 28.64143324753087,-96.208552463485731 28.662298487953962,-96.214659365527126 28.665351929112688,-96.192267404524671 28.68774389011514,-96.1912495908051 28.694359708878277,-96.195829762405154 28.698939880478335,-96.202445561444364 28.700975507917487,-96.208747675276499 28.700187749031503,-96.21684000834783 28.699176214258408,-96.221467818495611 28.69859774191346,-96.222801895007663 28.698430983480506,-96.223384071149837 28.698294,-96.224163927167865 28.698110503315906,-96.227000018566372 28.697443183508955,-96.229623268039219 28.696825944469072,-96.231453341209956 28.69639533631743,-96.233964222112746 28.695240331316239,-96.23422531976 28.695120226420762,-96.234426397558138 28.695027730650764,-96.243315632596989 28.690938683290845,-96.256898753233088 28.684690448956413,-96.263514562134276 28.683672635236839,-96.268603640594122 28.688761723558645,-96.287942160437836 28.68316371851509,-96.304227224329892 28.671458831154069,-96.305245042980445 28.660262850652849,-96.303866760710434 28.646480081370939,-96.303718312539132 28.644995605411349,-96.322902111893882 28.641863561491792,-96.322903115546453 28.641863397630402,-96.328654788116609 28.640924350533034,-96.373438710121519 28.626674919011112,-96.376492171004159 28.620059100247982,-96.384634680760783 28.615987845369677,-96.473693647496702 28.573239550803915,-96.48794308888057 28.569677192923439,-96.482854000558774 28.580364266564878,-96.480309456397848 28.596649335387912,-96.485907441717487 28.60784531588914,-96.490487633041468 28.610898766909834,-96.510843966604781 28.614970031650095,-96.510335069606953 28.617514575811001,-96.497612348802434 28.625148188569788,-96.496594535082863 28.630746183751381,-96.49964797624159 28.635835272073191,-96.506263795004728 28.638379806372129,-96.513590449607946 28.639711925390898,-96.518002756430107 28.640514162994929,-96.524548246905439 28.641704252172264,-96.541744210187403 28.644830790950802,-96.545449731689985 28.645504522133091,-96.555118991611849 28.64601343885484,-96.5632615210924 28.64448670841351,-96.564664053901609 28.647882315216158,-96.572092291837293 28.665866475800886,-96.572930781014264 28.667896502859467,-96.570386236853366 28.674003385176928,-96.559190266214102 28.687235002979271,-96.559699163211917 28.6913062775815,-96.561225893653244 28.696395346179383,-96.566823878972883 28.697922096344637,-96.575158129308363 28.702846874961025,-96.578019859474111 28.704537895383844,-96.57828826199534 28.705826226653553,-96.579938546079447 28.713747585140421,-96.580564403635009 28.716751699466613,-96.584126761515478 28.722858581784067,-96.584439828543964 28.722940967911413,-96.591359183115401 28.724761852179114,-96.593796021437342 28.725403125944972,-96.611342043876562 28.720366765465901,-96.616906294097589 28.718769618875786,-96.625254999999996 28.716373230030175,-96.632357663078409 28.714334501780041,-96.638120426114043 28.71268037463507,-96.643733366943522 28.711069252030907,-96.64589364157122 28.710449172933409,-96.648758110223909 28.709626963981723,-96.65548660082564 28.704200766225345,-96.664534272187169 28.696904262901135,-96.657918463285995 28.687743919701024,-96.642136214348383 28.67476740345478,-96.635017595423747 28.668914316579048,-96.634564255386636 28.662567599984854,-96.634358790297441 28.659691108644115,-96.634304719917793 28.658934128568227,-96.633999771842213 28.654664885057134,-96.627892869800831 28.650084693733149,-96.626425176648581 28.649921620227584,-96.623312698200763 28.64957579673532,-96.621378075676532 28.648286046719594,-96.615940371111847 28.64466090565978,-96.615679075580019 28.64448670841351,-96.614055987266909 28.642701311583636,-96.613585709918453 28.642184006591457,-96.613038272810982 28.641581825879328,-96.612716570908134 28.641227953848528,-96.611999586497333 28.640439271135588,-96.610589997120172 28.638888723093881,-96.610589997120172 28.638695619081329,-96.610589997120172 28.63634418879494,-96.61975034032028 28.62769274259265,-96.620390401117646 28.626519297452973,-96.620672695530928 28.626001757543317,-96.621575870071126 28.624345937066781,-96.622336527533278 28.62295139797671,-96.622803791340985 28.622094747411062,-96.621924089240224 28.619379144726071,-96.621515564809087 28.618118047314677,-96.621338841511317 28.617572510068054,-96.620571817957583 28.61520474122884,-96.620437729127232 28.614790814755992,-96.617253050911174 28.604959849584038,-96.615239196090883 28.598743166058551,-96.614649885719274 28.596923990196654,-96.613008078443599 28.591855801497097,-96.611528402529856 28.587288105363601,-96.611113505533794 28.586007336117376,-96.611098903979951 28.585962261746474,-96.608298572876635 28.583628658523324,-96.608045443097311 28.583417717585569,-96.607377235577218 28.583437664220053,-96.600365219155734 28.58364697962633,-96.593251058093557 28.583859344147964,-96.573948594733835 28.584435541167107,-96.565297148531556 28.582399903865994,-96.564279334811971 28.57629300182461,-96.563658896232639 28.575155527088082,-96.562968608029692 28.573889994257026,-96.561225893653244 28.570695006643017,-96.557566061723506 28.569051817003789,-96.543745727976315 28.562846769979732,-96.536289388489891 28.559499026141786,-96.535271536438941 28.559346348925885,-96.526111211846271 28.557972305562419,-96.524846286909124 28.55686549700842,-96.523547686998668 28.555729222873186,-96.522039937244045 28.554409942750958,-96.516783301381025 28.541093122164042,-96.514406314623301 28.535071417976255,-96.512075206364955 28.532603188828926,-96.505754868421008 28.525911074776143,-96.496773944100411 28.520225902118948,-96.493684489370622 28.518270192113103,-96.482894459981281 28.511439806078794,-96.464303363514816 28.49967112954555,-96.450283853050735 28.49079639296917,-96.41974938229167 28.46738661824714,-96.410828816467941 28.459457253614527,-96.410588999643707 28.459244083835621,-96.409758652296418 28.458206146634446,-96.408886828651134 28.457116363910039,-96.40506625078595 28.452340627696451,-96.402446489887097 28.449065917053971,-96.402758256176753 28.447714917044181,-96.403973200604497 28.442450108152798,-96.407195206365273 28.441281062045864,-96.417343901660857 28.437598792799108,-96.461479843413926 28.421584870195201,-96.476120924474287 28.411702150055138,-96.481836236149007 28.407844318412668,-96.504737094149291 28.397666161492985,-96.511137484289378 28.396220910815867,-96.520513236388609 28.394103803612502,-96.534249520963641 28.388796609353736,-96.542905217114992 28.385452367272176,-96.559699173073881 28.377818734789468,-96.57038624671533 28.368658381727396,-96.577905378446388 28.364719789411506,-96.5917603939982 28.357462401226169,-96.600411840200493 28.354408960067438,-96.650793747525029 28.34677533744669,-96.672676831253582 28.335579347083499,-96.688452973492915 28.347284234444516,-96.694559875534281 28.347284234444516,-96.698122233414779 28.342704062844462,-96.705246949175717 28.348810964885843,-96.700157860853906 28.369676195446971,-96.705755865897487 28.400210705653887,-96.710336037497541 28.406826524417021,-96.710425531143088 28.406841439843959,-96.711757514930511 28.407063434453107,-96.711949596522956 28.407095447664108,-96.71209813364915 28.407120203551969,-96.7125191933293 28.40719037931537,-96.712878460543081 28.407250256459115,-96.722549831718325 28.408862132132249,-96.749013067323034 28.408862132132249,-96.762244685125353 28.411915593014903,-96.76554491144303 28.411090543097366,-96.768351577304784 28.410388882297497,-96.775985199925543 28.405808690973519,-96.777118787058129 28.404067826336821,-96.778115367638478 28.402537364460464,-96.780337941159374 28.399124129135416,-96.780820705165937 28.398382742114745,-96.790234641309425 28.383925636830853,-96.794392274787313 28.366371177405831,-96.794814812909479 28.364587126849088,-96.794810066932399 28.364444747076842,-96.79477772408616 28.363474458555832,-96.794305906049686 28.349319871745632,-96.794064195689629 28.347593374049037,-96.792754716842737 28.338239980125699,-96.790743538307225 28.323874459722486,-96.791161640090152 28.319066463411133,-96.791737095961722 28.312448960638157,-96.791761391474679 28.312169572361466,-96.791798306096766 28.312130020933203,-96.806010803272656 28.29690232711997,-96.809573161153153 28.290286508356836,-96.806010803272656 28.282143978876302,-96.799480493673911 28.2729662335258,-96.799349781293685 28.272782529381054,-96.787181219874626 28.255680743271615,-96.787181219874626 28.250082757951983,-96.800412817953031 28.224128419345121,-96.810027933605966 28.21709297064087,-96.81015126416365 28.217002728792142,-96.823379937963566 28.207323213817581,-96.836184503007971 28.197954022244446,-96.84100213695676 28.194428925121734,-96.842143298799229 28.193593928862132,-96.847274602334139 28.190686192954498,-96.857267971405633 28.185023289193378,-96.872677809006134 28.176291056181483,-96.877474270970822 28.171878306563691,-96.886067200578026 28.1639728030657,-96.898123211167331 28.152881261735526,-96.906497631141988 28.149042991548708,-96.910337015250093 28.147283276415891,-96.926704620510804 28.131597659113019,-96.934764623415617 28.123873491831901,-96.962356663895278 28.123371819605953,-96.962754569737697 28.123364584972112,-96.979717515560068 28.129783000626698,-96.995398793779131 28.135716460690723,-97.000413785843605 28.137614026355994,-97.007520616950742 28.136091128354632,-97.007538501604586 28.136087295914667,-97.009222611239821 28.135094102666635,-97.00939982996681 28.134989589017771,-97.027385928308092 28.12438239869169,-97.028912648887456 28.117257682930727,-97.025203084589847 28.111384210119862,-97.023365428929196 28.108474590635566,-97.022805746846075 28.107588427939849,-97.02290356380766 28.107197159145706,-97.023379876854563 28.105291902342927,-97.02382356056566 28.10351716319958,-97.025496807419856 28.101530180660298,-97.031966099908146 28.09384788848477,-97.033883146887263 28.088918342142531,-97.035528457788629 28.084687545284659,-97.035528457788629 28.083043098067591,-97.035528457788629 28.081818766572983,-97.035528457788629 28.074000471643224,-97.033022532693735 28.061470870449408,-97.032801767679686 28.060367047518316,-97.031459273912688 28.053654591691117,-97.031457183186404 28.053644138079921,-97.025859197866765 28.041939250718904,-97.030948266464648 28.033287814378582,-97.03239348781301 28.032603236465789,-97.040617526386512 28.028707642778524,-97.041168915404441 28.028259640036232,-97.046718327422383 28.023750751173218,-97.048760075590963 28.022091833877354,-97.050263648357429 28.019142519014494,-97.059727497080047 28.000578821719444,-97.061991693393324 27.99613751499442,-97.06790259446116 27.99219690579767,-97.073772658186698 27.988283521554418,-97.075732208193486 27.986977152070377,-97.083740513918784 27.975854507337193,-97.090858162154831 27.965968886660271,-97.094600599978094 27.960771057335059,-97.101378519421061 27.951357282114685,-97.101544336287489 27.951126980954953,-97.101629253046639 27.951009041034034,-97.112670327945679 27.935674217691009,-97.118292397880069 27.927865788706086,-97.121533983365822 27.923363587495643,-97.122089895488358 27.923104160785101,-97.123659587861624 27.92237163470331,-97.129167576400675 27.91980122961516,-97.134800331352182 27.902469771509406,-97.13578341488774 27.899444915775788,-97.139044920333077 27.897526377912126,-97.141761509630356 27.895928379836029,-97.144434841366106 27.894355827453982,-97.155121915007527 27.880615312653809,-97.156735458496229 27.877916799393841,-97.171211094589736 27.85370753808861,-97.17159000749372 27.853073838716419,-97.18273536107327 27.834434189625789,-97.184638591770963 27.831251199324921,-97.187183135931861 27.824126483563962,-97.18941247112663 27.823657146727278,-97.196852395853725 27.822090836400886,-97.201135366472329 27.822090836400886,-97.208766105658313 27.822090836400886,-97.209575096934316 27.822090836400886,-97.21103842611052 27.822356897891609,-97.21191517248603 27.822516307306422,-97.214117494684729 27.822916731993345,-97.215541825234411 27.823175702780983,-97.217387510601711 27.823511284007832,-97.220771087297493 27.824126483563962,-97.222189700736266 27.82464074101355,-97.223091414240102 27.824967618564596,-97.225175600069406 27.825723150734085,-97.225442194074731 27.826156365185522,-97.225555528799433 27.826340533769983,-97.225696025109201 27.826568839847944,-97.22598639723725 27.82704069367685,-97.227317456819009 27.829203661466853,-97.227317456819009 27.832884543697009,-97.227317456819009 27.832951908184537,-97.22711696094737 27.834288541284948,-97.22651425924083 27.838306534493725,-97.227537582741903 27.841230302974814,-97.228388390382094 27.843661171179477,-97.233100025453197 27.847817700825228,-97.234045759551762 27.848652012430087,-97.234511621821596 27.849062988727319,-97.238500481248579 27.854279199579018,-97.239439164419366 27.855506710708827,-97.241127420860792 27.857714434929608,-97.24139627838295 27.858969111470945,-97.242350826195675 27.863423696705052,-97.242654131578206 27.86483913096664,-97.243132066142749 27.865496290124597,-97.244364366034702 27.867190700237273,-97.250796680782656 27.876035121329831,-97.263010484865433 27.88010639593206,-97.267085449659064 27.88068852838779,-97.272090543185925 27.881403535150675,-97.273697558506882 27.881633106649463,-97.276628649874411 27.881144591421538,-97.283916343274228 27.879929975854907,-97.291452123510041 27.878674012482271,-97.291709327200167 27.878631145200583,-97.295071705789752 27.87807074876898,-97.29826066531912 27.876989746639367,-97.302276298241679 27.875628516526195,-97.30641171632756 27.874226681314273,-97.315889353880934 27.871013926092836,-97.325097299274915 27.867892591849294,-97.326845878314657 27.866807265961079,-97.334190606350404 27.862248465187459,-97.346213303335873 27.854786094892546,-97.354613966176373 27.849571885725148,-97.359768343966266 27.850509060182219,-97.360211961357962 27.850589719168646,-97.360654441244435 27.850290746360063,-97.363614400263117 27.848290774636723,-97.376904444631478 27.839311017562139,-97.379041564479934 27.837867018088055,-97.379057154646048 27.837837708581311,-97.379081675001302 27.837791610322164,-97.37968928125764 27.836649310776913,-97.391764280353456 27.813948316782309,-97.391812130016405 27.812975373996721,-97.391812459346511 27.812968677620422,-97.39203202611381 27.808504155006624,-97.392068018906713 27.807772301822137,-97.392095751995882 27.807208395884746,-97.393123788240075 27.786304999999999,-97.393168763213623 27.785390509210199,-97.393291005863816 27.782904909577571,-97.393142269808052 27.782564941361908,-97.39298939956177 27.782215523565412,-97.390949955785928 27.777553936582201,-97.390465068726371 27.776445623015572,-97.390185233729682 27.775805999999999,-97.389524844304646 27.774296538065283,-97.38835420640342 27.771620793596586,-97.388306220610531 27.771511111755789,-97.388011229692253 27.770836846624743,-97.38704242749894 27.768622441036733,-97.386166290102864 27.766619840754537,-97.385225495384532 27.765302728148875,-97.378862356737287 27.756394334042721,-97.375764970087644 27.752057992733249,-97.373069654276961 27.748284550598278,-97.368354500700462 27.741683335591173,-97.365855345900911 27.739779218033028,-97.354970329043653 27.731485873530172,-97.352272255056832 27.729430198526604,-97.34997871774064 27.727682741876539,-97.347507961666906 27.725800261438444,-97.346980343555629 27.725398266768138,-97.343485766084669 27.723942192633796,-97.323096068004702 27.715446484002907,-97.316445853072636 27.712675560756566,-97.312489136070184 27.711663774861414,-97.307771479065892 27.710457406346261,-97.30518751069485 27.709796650788128,-97.285725857752936 27.704820043684109,-97.259850586867117 27.698203387982087,-97.253955150197811 27.696695845323848,-97.254014647303208 27.696526337654994,-97.255455364792269 27.692421723465429,-97.259957004258851 27.67959652118169,-97.261636792970123 27.679316557300702,-97.26241778482418 27.679186392412099,-97.266063906300232 27.678578707462119,-97.266172011465457 27.678349884642419,-97.272736281666539 27.664455499360063,-97.273042341106247 27.663807672923248,-97.273584380560237 27.662660354976065,-97.276535709391737 27.656413369610792,-97.277059923980644 27.655303780997631,-97.278846463786479 27.651522268106756,-97.280071982275985 27.648928251476995,-97.280889132874719 27.647198614380269,-97.282300269490932 27.644211705671331,-97.282869528026779 27.64300677394548,-97.287959956150573 27.632232024058997,-97.288756326795195 27.630546371240786,-97.290370933329072 27.627128784125372,-97.290610159422798 27.626622421740255,-97.291264204229464 27.625238025568656,-97.291996439564016 27.623688125953898,-97.292910903535031 27.621752508687905,-97.293983233844585 27.619482740684056,-97.29528946695001 27.616717877953022,-97.296598377059297 27.613947348891728,-97.297587499071795 27.609496333379006,-97.298634024222366 27.60478700569162,-97.29761621050281 27.598680093788271,-97.294053852622326 27.594099922188217,-97.294182769084571 27.593971005725969,-97.300049926927869 27.588103847882682,-97.302196382102863 27.585957392707677,-97.311120578488044 27.579146819865922,-97.321534901946578 27.571199044464009,-97.325080504595888 27.561034984604785,-97.336802147188081 27.527432946040641,-97.343417965951204 27.517763686118776,-97.347489240553443 27.503005347737066,-97.350542661988243 27.478577729709574,-97.359194108190536 27.458221396146271,-97.365809926953673 27.450587783387487,-97.371916828995055 27.425142361502377,-97.369881181831957 27.412419660421783,-97.372934622990698 27.401223670058592,-97.379550422029908 27.390027689557368,-97.399397858595393 27.344734850830708,-97.401942402756291 27.335574497768633,-97.404995863638931 27.329976512448997,-97.413138393119482 27.321325066246711,-97.42026310888042 27.317253791644482,-97.430441285524054 27.313691423902039,-97.450797599363412 27.313691423902039,-97.482858840011673 27.297915271800758,-97.508304242172855 27.275014394076553,-97.532222933616623 27.278576761818989,-97.544436737699399 27.284174747138625,-97.546981281860297 27.290790556039799,-97.536803105216677 27.289263825598471,-97.526624948296998 27.291808369759369,-97.524589320857856 27.297915271800758,-97.51746460509689 27.305039987561717,-97.504741884292372 27.305039987561717,-97.498126085253162 27.308602345442196,-97.502706237129289 27.322342870104325,-97.499143898972747 27.327940855423961,-97.483876634007316 27.33862793892736,-97.483876634007316 27.351350640007954,-97.486930094889971 27.358984272490666,-97.501688443133645 27.366617904973374,-97.514411144214236 27.361528796927644,-97.520518046255617 27.352877370449281,-97.538329835658018 27.335574478044709,-97.570899973304094 27.315727061203152,-97.584131581244463 27.309620159161771,-97.609068086407831 27.285192570720163,-97.621790807212349 27.287228188297352,-97.63146005727225 27.286210384439741,-97.63654914066305 27.282139109837512,-97.636657939024673 27.281797172172649,-97.640111498543547 27.270943129336285,-97.639093679892994 27.253131339933887,-97.635022415152719 27.247024437892502,-97.628915513111338 27.242953173152234,-97.597363199046811 27.242444266292445,-97.5826048606651 27.240408628991332,-97.573953414462821 27.238881903480983,-97.56123071338223 27.232775006370584,-97.542910007258072 27.229212648490105,-97.520009129533875 27.231248285791217,-97.509830972614182 27.235319550531486,-97.503215153851045 27.23989972213154,-97.500161712692318 27.244479898662579,-97.485148876501881 27.25084127385778,-97.467082638600573 27.253640266517596,-97.45843119239828 27.259492710198106,-97.450288657986775 27.262546171080761,-97.424079880742951 27.264072891660124,-97.422298701802717 27.257711541119829,-97.434766954384401 27.202240525749552,-97.444945121166043 27.144733882940127,-97.443672849085587 27.116235010034327,-97.452324285425917 27.115217201245734,-97.455886653168363 27.110382571284802,-97.456650008527049 27.09969549764336,-97.46173908698691 27.095624227972113,-97.475479621510999 27.098423220631929,-97.480568699970846 27.102494490303176,-97.491510231973166 27.101222218222723,-97.495835955074313 27.09409750739275,-97.4932914109134 27.078066891999608,-97.477515248950155 27.066107546277717,-97.479041969529504 27.06279964182713,-97.482256963728076 27.061942305056665,-97.48693005051112 27.057710553505324,-97.487693415731783 27.053639288765055,-97.486675602012198 27.034809685643079,-97.477515248950141 27.032519589981089,-97.473952881207694 27.029211690461484,-97.473443984209865 27.022850330059228,-97.478300083716704 27.000269498406993,-97.478533072531675 26.999186101907302,-97.480568690108868 26.997659376396957,-97.483967678435022 27.000330000000002,-97.484131057851314 27.000458369056773,-97.492988547644813 27.000330000000002,-97.49889841702128 27.000244349959733,-97.533497176854453 26.999742920066073,-97.536803065768822 26.999695008767091,-97.549271318350492 26.995878197456715,-97.555378215460905 26.990280207206101,-97.551052502221722 26.980865400714134,-97.549525776711377 26.965343697111763,-97.552324769371197 26.95211208177491,-97.555378205598956 26.947277449348487,-97.555378205598956 26.938880461507075,-97.540874325578116 26.906310323861007,-97.540110950495503 26.900966796902246,-97.547999041339082 26.895114353221743,-97.552324764440229 26.888498544320569,-97.552324764440229 26.875332999999998,-97.552324764440229 26.873831771434514,-97.552324764440229 26.871753101924,-97.552324764440229 26.867633303897481,-97.555396527456509 26.865969429990074,-97.558431656619632 26.864325399446898,-97.558453748966258 26.864224239771101,-97.559853702000524 26.857813929560987,-97.562641307980527 26.845049630589628,-97.563266286580571 26.842187886943357,-97.552579212939136 26.827938454188693,-97.547744582978211 26.824630549738107,-97.537566416196555 26.824885004400745,-97.509830908511418 26.8035108521869,-97.48438549155729 26.763561555211936,-97.478024141017002 26.757200199740662,-97.471662790476714 26.758726925251008,-97.468609339456009 26.740915130917632,-97.467337057513589 26.710126182073765,-97.444945096511148 26.633535472850511,-97.445708451869848 26.609362327976836,-97.441206258760758 26.5999011976768,-97.435205432977895 26.587290766879221,-97.432741093635642 26.582112082834445,-97.429217375703914 26.574707168454967,-97.428151110966368 26.572466467229631,-97.418145193925739 26.555638334425574,-97.416955130465126 26.553636864107652,-97.41864075483771 26.543121778291471,-97.42039414733226 26.532183948458435,-97.422284917775215 26.520389141863415,-97.422298667285844 26.520303371102887,-97.425861015304378 26.516741008291426,-97.430695645265303 26.506562841509776,-97.430695645265303 26.494603495787885,-97.42802638225659 26.488322868889494,-97.42636993202612 26.484425333937217,-97.429168909893022 26.478063961207511,-97.43553026043331 26.470175880225888,-97.441382709044788 26.466613522345408,-97.441382709044788 26.455417541844177,-97.43756589773443 26.449819546662585,-97.425861005442428 26.446002740283195,-97.421026375481503 26.446766095641895,-97.417209564171131 26.449819546662585,-97.411611568989528 26.447275002501684,-97.412883841069984 26.433025580841726,-97.421789740702152 26.417249413947498,-97.419499649971158 26.413178149207234,-97.406013578738921 26.409106884466965,-97.398125502688274 26.410888063407203,-97.394308686446919 26.414450416356704,-97.395072051667569 26.417249413947498,-97.382484933201752 26.411326066613285,-97.377769169124974 26.409106884466965,-97.369626639644437 26.394602999515151,-97.374461259743413 26.38086247238753,-97.388965149626202 26.365849678110436,-97.392018610508856 26.339386444971247,-97.391000786927322 26.332261729210284,-97.38794734576858 26.330480545339064,-97.376242448545611 26.336332993950553,-97.372171183805335 26.339895346900054,-97.36937219114553 26.348546788171358,-97.358176200782339 26.356434874083959,-97.343417852538664 26.35923386920927,-97.342332537534404 26.358759043566288,-97.335275323058127 26.355671510096041,-97.335020017473738 26.355402767481841,-97.331108052904881 26.351284911668415,-97.330440693097202 26.350582427937965,-97.333762617526986 26.340749518544904,-97.336802038706509 26.331752819885011,-97.343786761143846 26.325987658774913,-97.344677525679145 26.325252425399061,-97.352414066956698 26.31886671611711,-97.352832659030639 26.318521211944631,-97.354359379610003 26.313941040344577,-97.348998828460125 26.312092573442666,-97.347821984790102 26.311686765063648,-97.346980205488165 26.311396496183672,-97.34736083223585 26.297503848546928,-97.347437236753805 26.2947151295396,-97.347489122209922 26.292821341560611,-97.347051378510429 26.289694600417153,-97.344137846000223 26.268883651035111,-97.343926764329439 26.267375924606483,-97.342629246748771 26.266550231912309,-97.341127771669619 26.265594748131736,-97.335282658907644 26.265594748131736,-97.331967408745584 26.265594748131736,-97.330307576264516 26.266747410222209,-97.323817586106017 26.271254350355399,-97.322807065545476 26.271956101137519,-97.313207351206557 26.273518846137112,-97.312102101648151 26.273698770576502,-97.311865543405119 26.273737280077761,-97.307030913444194 26.253126493084562,-97.308048727163751 26.249055213551365,-97.321280340035131 26.236078054109896,-97.321280340035131 26.228698889850026,-97.304486359421333 26.202490107675235,-97.296598288301666 26.200708923804015,-97.294817109361432 26.192311940893585,-97.296089381441874 26.182388227541828,-97.303096384204423 26.167373221160254,-97.305986772892751 26.161179530923267,-97.306776455083323 26.159487354748606,-97.300965235160419 26.149753561518516,-97.296881711355638 26.142913659244432,-97.296598288301666 26.14243892563589,-97.285549237329036 26.12861437636975,-97.28536038956149 26.128378090441405,-97.284582435540926 26.126454238998875,-97.282094393487881 26.120301403270393,-97.282839179410786 26.118439442973433,-97.28311220967295 26.117756868971455,-97.285501587752023 26.116923364107649,-97.294053737977038 26.113940052730097,-97.295071554162092 26.108342057548505,-97.292023254217796 26.105090538920617,-97.291924538345086 26.104985242032239,-97.291541272704407 26.104576425513905,-97.287190793518263 26.099935916255493,-97.286650074642992 26.099359149688052,-97.286603231473521 26.09930918366079,-97.283195451762182 26.095674220102872,-97.282639002190891 26.095080674133143,-97.282108002623801 26.094514274823585,-97.280435495761154 26.092730268223651,-97.279905974795895 26.09216544608875,-97.27980430522237 26.092056998587434,-97.27089841052117 26.086459003405839,-97.267086875013263 26.085485845069449,-97.24859983335169 26.080765747704277,-97.246979719077387 26.080352101364454,-97.229515030031649 26.08000965739204,-97.220290685310772 26.079828788368793,-97.208048240752987 26.079588741074772,-97.205005053278043 26.078666562185607,-97.199651252911579 26.077044196913871,-97.199152512570265 26.073220535461079,-97.199134106850465 26.073079425477676,-97.198725011196004 26.069943037351702,-97.198302051934135 26.066700361972018,-97.196018989165054 26.049196947106083,-97.195939794821285 26.0485897927724,-97.195071061587612 26.04192952989985,-97.204994779870347 26.030224637607851,-97.214918488291119 26.030733549398622,-97.224842206573854 26.027425644948035,-97.226114478654296 26.024372193927345,-97.219244211392265 25.99612778184791,-97.216954125592238 25.993837693582392,-97.208557137750816 25.991802058746771,-97.195834416946283 25.993074335758202,-97.174460269663413 26.000071824804216,-97.167208324722026 26.007069313850227,-97.162755377371425 26.014575709756024,-97.16262814819099 26.023481604457221,-97.172042954682937 26.044728528107019,-97.178658763584124 26.045491893327686,-97.182730028324386 26.053125515948434,-97.164981847348457 26.063876207878327,-97.152009000000007 26.062107999999998,-97.152012551274964 26.062039393270581,-97.15321 26.038906,-97.151921999999999 26.017652999999999,-97.14747181118706 25.985075937251509,-97.145567 25.971132,-97.146880999999993 25.969781,-97.146293999999997 25.955606,-97.147784999999985 25.953132,-97.156608000000006 25.949021999999999,-97.160293999999993 25.950243,-97.168198638692317 25.959262149012673,-97.178362000000007 25.962114,-97.187583000000004 25.958174,-97.206945000000005 25.960899,-97.214339285966162 25.960186817526893,-97.227626420907342 25.958907063854085,-97.229225999999997 25.958753000000002,-97.239867000000004 25.954974,-97.244841570811701 25.950784663302475,-97.248032999999992 25.948097,-97.255343503388133 25.949129556975723,-97.276707000000002 25.952147,-97.28138899999999 25.948036999999996,-97.277163000000002 25.935438,-97.284201820237172 25.935775045516863,-97.290083634347766 25.936056689174489,-97.293963761326751 25.936242484429805,-97.303601999999998 25.936703999999999,-97.316138101068788 25.931602038234757,-97.320560999999984 25.929801999999999,-97.324914000000007 25.924040999999999,-97.332235861490744 25.923541683060932,-97.33463605770983 25.923378000829199,-97.338346 25.923124999999999,-97.339310160867683 25.923294280152341,-97.350397999999998 25.925241,-97.367642000000004 25.915679999999998,-97.369283543255307 25.913688286645449,-97.373641276386991 25.908400972256459,-97.374430000000004 25.907443999999998,-97.372365000000002 25.905015999999996,-97.365976000000003 25.902446999999999,-97.365883578347024 25.900015396466173,-97.365521 25.890476,-97.364300486696564 25.885628504434479,-97.362421560218849 25.878165998501085,-97.360082000000006 25.868874000000002,-97.364783621478566 25.852096838905283,-97.36542 25.849826,-97.372864000000007 25.840116999999996,-97.394513000000003 25.837377,-97.422635999999997 25.840377999999998,-97.42853949246836 25.842912007889609,-97.434188204901034 25.845336654308195,-97.441181027463472 25.848338244915578,-97.445113000000006 25.850026,-97.445601387363794 25.851485440010176,-97.447179184935308 25.856200346812667,-97.448271000000005 25.859463000000002,-97.449172000000004 25.871677999999996,-97.452166849899044 25.875807172885114,-97.453724626189043 25.87795496921364,-97.454727000000005 25.879337,-97.45488774919265 25.879394304385261,-97.462586893331732 25.882138919861518,-97.468261999999982 25.884162,-97.468599721544905 25.884059457735212,-97.481532785459223 25.880132596436578,-97.486059999999995 25.878758,-97.490359999998347 25.879275544671589,-97.494739403174762 25.879802646248233,-97.496860999999996 25.880057999999998,-97.519591990380178 25.88590026892226,-97.521761999999995 25.886458,-97.52344767329889 25.891064017588189,-97.524375103029939 25.893598172727145,-97.528116959024402 25.903822606212749,-97.528119935206945 25.903830738482007,-97.52812430798204 25.903842686870224,-97.528627999999998 25.905218999999999,-97.528849446448064 25.906732522417784,-97.530321999999998 25.916796999999999,-97.530415259581318 25.916820899843632,-97.539878370214808 25.919246032588486,-97.542957 25.920034999999999,-97.545169999999999 25.923974999999999,-97.545468770003851 25.926387609575503,-97.545470694802859 25.92640315259677,-97.546397824784293 25.933889856891241,-97.546420999999995 25.934076999999998,-97.546611329705271 25.934141994258589,-97.555160182125618 25.937061277530951,-97.555378999999988 25.937135999999999,-97.555477252221124 25.937015705749829,-97.559364000000002 25.932257,-97.582565000000002 25.937856999999997,-97.580418999999992 25.945115999999995,-97.583044 25.955442999999999,-97.598043000000004 25.957556,-97.5981230356591 25.957681442330628,-97.607733999999994 25.972745,-97.60783562843973 25.973457509771446,-97.607844088251909 25.973516820913719,-97.608283 25.976593999999999,-97.609461531117333 25.977846566488182,-97.613191727531174 25.981811093986448,-97.613466053312663 25.982102652924251,-97.616041456343709 25.984839842874308,-97.624938181905222 25.994295461083137,-97.627225999999993 25.996727,-97.634804000000003 25.999508999999996,-97.635072411453706 25.99971613545971,-97.639164724998494 26.00287420951236,-97.642117661027001 26.005153015998879,-97.644011365977178 26.006614404624422,-97.643848619841975 26.01214811069886,-97.643707610985203 26.016942704231127,-97.649175518723894 26.021499311673544,-97.65096419129361 26.02118629315062,-97.659123404318109 26.019758427116106,-97.661326460130226 26.019372891335045,-97.66298585459532 26.019511685247039,-97.668297999999993 26.019955999999997,-97.669519566971061 26.021667429940447,-97.671350987084779 26.024233271429612,-97.671567999996611 26.024226704244594,-97.691453959129774 26.023624920821636,-97.697068999999999 26.023454999999998,-97.703247203861338 26.030308742132775,-97.706066818770353 26.031284762516545,-97.709294717923569 26.032402112038366,-97.711145328137576 26.033042707775564,-97.719920000000002 26.030837999999999,-97.723585926686539 26.030959832537771,-97.729354247932832 26.031151535557555,-97.735180242251758 26.031345155270547,-97.758837769919694 26.032131383932391,-97.76309059882324 26.033650253079866,-97.763351431657455 26.034258862745556,-97.76491324062286 26.037903081983409,-97.769788888528993 26.041344719068825,-97.770077387482857 26.041548365582649,-97.776788595669075 26.042443189872706,-97.779190602367677 26.042763456191249,-97.784050976575529 26.040637041739476,-97.789822674626535 26.0424596835391,-97.792252861730461 26.04428232533872,-97.793102878401371 26.047342389307317,-97.793537334820812 26.048906434437896,-97.794638769549053 26.052871604582215,-97.795290594138677 26.055218176136449,-97.801344 26.060016999999998,-97.803973303988229 26.059548218630308,-97.8082126910423 26.058792373859696,-97.819424117916668 26.056793476786609,-97.820997703829306 26.056512920501468,-97.825546000000003 26.055702,-97.826721102251625 26.055271667399982,-97.830409376527257 26.053920989485444,-97.836607999999984 26.051650999999996,-97.848759348822554 26.052295281844582,-97.85186756815483 26.052460084066457,-97.859824237628374 26.052881958029587,-97.860225497621784 26.05290323340671,-97.860503999999992 26.052917999999998,-97.861874999990562 26.053580848914272,-97.869705222636284 26.057366592615971,-97.871187000000006 26.058082999999996,-97.876982999999996 26.064482999999999,-97.886529999999993 26.066338999999999,-97.901546631929207 26.060958662441269,-97.905109129229899 26.05968224852101,-97.913882 26.056539,-97.935419999999993 26.052687999999996,-97.944344999999984 26.059620999999996,-97.950095000000005 26.061827999999998,-97.96210735288345 26.054793018383155,-97.96735799999999 26.051718,-97.971403108419892 26.054550391225565,-97.978769 26.059707999999997,-97.979877702655912 26.062937323324391,-97.981335 26.067181999999995,-98.010970999999998 26.063862999999998,-98.015122209308288 26.064471399070538,-98.026123959861238 26.06608380989196,-98.028288992822496 26.066401115993269,-98.028758999999994 26.066469999999999,-98.029241090705753 26.065867136858621,-98.033102 26.061039,-98.034402999999998 26.051375,-98.034479464439173 26.051215303797409,-98.034525269179767 26.051119640464091,-98.039238999999981 26.041274999999995,-98.054365285842039 26.044575736209502,-98.070020999999997 26.047992,-98.070022345126233 26.049160242153427,-98.070025 26.051466,-98.071425788568789 26.055027814897404,-98.076094939927231 26.066900165398668,-98.076139697069991 26.067013970337847,-98.076205751241318 26.067181927684643,-98.076543999999998 26.068041999999998,-98.076994427275579 26.068371469710563,-98.080289992888041 26.070782045417982,-98.080494999999999 26.070931999999999,-98.080906883185932 26.070920010911959,-98.084755 26.070808,-98.085506402845155 26.069709056167966,-98.085848999999982 26.069208,-98.085628527723046 26.069048386721494,-98.081567000000007 26.066108,-98.081855064865067 26.063941606819231,-98.081884000000002 26.063724,-98.082307152201508 26.063513440869801,-98.091037999999998 26.059169,-98.093593108176947 26.058759459973995,-98.094431999999998 26.058624999999996,-98.095078111969258 26.058956205325877,-98.096949561841043 26.059915534659098,-98.097643000000005 26.060270999999997,-98.105504999999994 26.067537000000002,-98.122952624574893 26.063250384797335,-98.12786358062813 26.062043837809401,-98.128331000000003 26.061928999999999,-98.142925292417388 26.051941751725515,-98.14645163947533 26.049528582072455,-98.146621999999994 26.049412,-98.146852932570667 26.049588146033326,-98.149462999999997 26.051579,-98.149462999999997 26.055813,-98.151730999999998 26.058187,-98.158277994458601 26.062311711597108,-98.16142849281799 26.064296576133319,-98.161911645017582 26.064600969774318,-98.167608590156348 26.068190136655492,-98.172071527808072 26.071001859012309,-98.177897000000002 26.074672,-98.179863281323136 26.072661506851002,-98.189059999999998 26.063257999999998,-98.191534000000004 26.057117999999999,-98.197046 26.056152999999998,-98.200871000000006 26.059161,-98.203328791159265 26.063523594334541,-98.204415309491765 26.065452171017675,-98.20496 26.066419,-98.205720285634712 26.066905180236589,-98.2057544964389 26.066927057036718,-98.220672999999991 26.076467,-98.228363119990576 26.077028417928002,-98.230097 26.077155,-98.231072909449267 26.07694353295701,-98.240214327563166 26.074962705064888,-98.248806000000002 26.073101,-98.250234708208026 26.074229377516481,-98.260964088277859 26.082703320039151,-98.264514000000005 26.085506999999996,-98.272091468517374 26.0934369782697,-98.272527821536428 26.09389363077193,-98.277218000000005 26.098801999999999,-98.277020629370639 26.099144109090908,-98.272932087266241 26.106230915405163,-98.272897999999998 26.10629,-98.272099931737145 26.106512924095771,-98.270258188934392 26.107027377392626,-98.270033999999995 26.107089999999999,-98.269661374787887 26.108231250649609,-98.268046405073434 26.113177467856264,-98.266755660647689 26.117130670341034,-98.265753950746401 26.120198637935399,-98.265698 26.12037,-98.266046753689267 26.120369439652073,-98.268388964369507 26.120365676386069,-98.271048543344023 26.120361403199535,-98.2713587738927 26.120360904747326,-98.279433560913844 26.12034793086255,-98.289509742149562 26.120331741306838,-98.296194999999997 26.120321,-98.299522999999979 26.11749,-98.299575546893735 26.117376878214852,-98.299577897643644 26.117371817572689,-98.299619756950364 26.117281703787395,-98.301477861619162 26.113281617347607,-98.302978999999993 26.110049999999998,-98.307788398559808 26.112633359128559,-98.31093219000293 26.114322040617914,-98.314784784069062 26.116391454064445,-98.31781148535309 26.118017240801439,-98.323055746360907 26.120834185452331,-98.323827999999992 26.121248999999995,-98.324165904650499 26.121735183484471,-98.335204000000004 26.137616999999999,-98.337914801547555 26.149187638321976,-98.338210450252006 26.150449569219312,-98.338419999999999 26.151344000000002,-98.338123748811455 26.151776768193923,-98.337139471603692 26.153214615149455,-98.336278203634194 26.154472768358826,-98.335109254490916 26.156180386856505,-98.334230366689056 26.157464279382136,-98.333315999999996 26.158799999999999,-98.333279392301918 26.159609030127591,-98.333194378655719 26.161487831708563,-98.333156000000002 26.162336,-98.33332593363771 26.162525092143447,-98.336569396995017 26.166134227137082,-98.336837000000003 26.166432,-98.337709251548077 26.166391430160555,-98.345513373134324 26.166028447761192,-98.345781000000002 26.166015999999999,-98.345955918391979 26.165759937155421,-98.34781294946896 26.163041431373035,-98.354645000000005 26.15304,-98.380009845764519 26.156864235849298,-98.386243919298991 26.157804141722139,-98.386694000000006 26.157872,-98.386714843024777 26.157901012682096,-98.387178289965661 26.158546112849205,-98.389418830831929 26.161664858836595,-98.394322667090904 26.168490808715738,-98.402249554153599 26.179524728065878,-98.404432999999997 26.182563999999999,-98.41082579547016 26.183537375155975,-98.418120000000002 26.184647999999999,-98.44253599999999 26.199151,-98.444302622216625 26.201317032456906,-98.444376000000005 26.201407,-98.444366742625562 26.201566115583965,-98.444174812576264 26.204865005795593,-98.443681770155465 26.213339409994948,-98.45054565332704 26.219516919037407,-98.450975699346984 26.219903961344286,-98.465077324681431 26.222335272645303,-98.467962758220992 26.221802674698019,-98.47639547470331 26.220246150374404,-98.481645999999998 26.219277000000002,-98.483269000000007 26.216439,-98.496684404575589 26.212853148677059,-98.500574513964963 26.213825676024403,-98.503492081872309 26.214798198660183,-98.504399410834864 26.216045775617395,-98.509275818143593 26.222750833698164,-98.509327236533252 26.222821533963195,-98.516621175147847 26.223550930651591,-98.520846190978133 26.222726536052498,-98.524733007990875 26.221968131567948,-98.526589565145571 26.221605875956907,-98.528323413163747 26.222421776495104,-98.535240999999999 26.225677,-98.538016739096946 26.231331135295655,-98.538505426714039 26.231595841236214,-98.543851889046309 26.23449184328507,-98.556093449912439 26.231976454911809,-98.561600478976516 26.23084487397777,-98.564343479612191 26.231667774301364,-98.575891642961039 26.235132223865481,-98.57618836327309 26.235221239973473,-98.581780384919284 26.243001439905978,-98.583095590242166 26.247416774401941,-98.585184223567666 26.254428618568909,-98.588002268837087 26.255070784392117,-98.599153999999999 26.257611999999998,-98.610401443364651 26.253223367217647,-98.613465000000005 26.252027999999999,-98.617434734670965 26.252082931217494,-98.618976176235122 26.252104260920568,-98.625299999997523 26.252191766854153,-98.626653663614832 26.252210498179227,-98.633391522135483 26.243617562727948,-98.63418 26.242612,-98.636673745354344 26.241784277127035,-98.654221000000007 26.235959999999999,-98.669397000000004 26.236319999999996,-98.675206000000003 26.239989,-98.678410535728389 26.244637915883345,-98.679041999999995 26.245553999999998,-98.678977427258005 26.246060764449961,-98.677766000000005 26.255568,-98.679196310064967 26.258571609080832,-98.681167000000002 26.262709999999998,-98.687156000000002 26.26512,-98.698855915315121 26.265619481498664,-98.707451416076225 26.272152066874316,-98.710601999999994 26.279018,-98.709170532219119 26.284185773270103,-98.710647239525585 26.288123668959596,-98.711233447604599 26.289686894290245,-98.722550749196131 26.295571625529284,-98.729196000000002 26.299026999999999,-98.73263614407044 26.29899082409209,-98.734613221934339 26.298970033513189,-98.745271658069271 26.303095890935232,-98.745297595683382 26.304159357241051,-98.745599830797417 26.316551278053844,-98.745615470637418 26.317192526042046,-98.748245116157776 26.32061106368975,-98.74905366960931 26.321662182706675,-98.754840366886953 26.324877004144408,-98.755242435754027 26.32510037501579,-98.766685638772316 26.325769085950686,-98.779858354339893 26.326538865087553,-98.779911999999996 26.326541999999996,-98.779946859358333 26.326559704051515,-98.789821999999987 26.331575,-98.796251999999996 26.349104,-98.797592059971961 26.356670995104565,-98.798210999999995 26.360166,-98.807348000000005 26.369420999999999,-98.808280016253249 26.369491024329768,-98.813413043374013 26.369876679389535,-98.81818280466733 26.370235041528161,-98.81932575812273 26.370320914010964,-98.824571000000006 26.370715,-98.828353477559162 26.367368473620303,-98.832909 26.363337999999999,-98.838554497099707 26.360957856802237,-98.842229804437778 26.359408346173527,-98.844057000000006 26.358637999999999,-98.847707 26.359594999999999,-98.853132332741467 26.364754198689674,-98.853414999999998 26.365023,-98.853852117327648 26.365076242531281,-98.854321671505204 26.365133435992636,-98.861354000000006 26.365989999999996,-98.861661690067152 26.365902494757481,-98.869113443480302 26.363783259984523,-98.874117355796216 26.362360176799562,-98.876164212939671 26.361778062685843,-98.882912762903771 26.359858814831277,-98.890964919192157 26.357568829017531,-98.895015448091129 26.359360408468842,-98.900432100164338 26.361756234493352,-98.900829697862818 26.361932094951143,-98.905559653818443 26.364024190108832,-98.91296803413384 26.372226331500926,-98.915344992389564 26.37485796579432,-98.921277034399395 26.381425588572444,-98.922831447878195 26.38166472803821,-98.923508557916591 26.381768898347495,-98.924925720775448 26.381986922427696,-98.926689551972586 26.38116380140492,-98.934437533628781 26.377548077521784,-98.937555770591459 26.376092900630621,-98.942046463189357 26.375531566775365,-98.950185820407469 26.380302920861933,-98.952073083609605 26.383491745197549,-98.952938578460817 26.384954133189183,-98.953327659277917 26.385611545667039,-98.958325183064531 26.394055638388448,-98.960041959595969 26.394835991082342,-98.96758721887106 26.398265653180793,-98.981979903868819 26.396488780147621,-98.98323657723455 26.396333635432413,-98.985344372930967 26.396073413984297,-98.99032130527651 26.395458978465552,-99.008003378826189 26.395458978465552,-99.014739404125635 26.398826987036053,-99.018845438188137 26.404005475794783,-99.021934999999999 26.407902,-99.025336792347318 26.409271761295809,-99.030462148109507 26.411335530401477,-99.031104888479149 26.411594335405351,-99.032315999999994 26.412082000000002,-99.033086386506682 26.412180127570064,-99.037216923343138 26.412706252494743,-99.039107 26.412946999999999,-99.039645291109963 26.412681959983438,-99.045466000000005 26.409815999999999,-99.053184999999999 26.402006,-99.062093000000004 26.397371,-99.082001999999989 26.396509999999996,-99.085125999999988 26.398782,-99.089412999999979 26.408099999999997,-99.092044248326658 26.410330707587079,-99.0977332288968 26.415153685331873,-99.099649490160544 26.416778244479925,-99.110855 26.426278,-99.113807999999992 26.434002,-99.110485406379198 26.436329519428725,-99.103082999999998 26.441514999999999,-99.097481906444941 26.458865277747179,-99.094712087984234 26.467445230774196,-99.091634999999997 26.476977000000002,-99.105030999999997 26.500335,-99.114051328117867 26.510193091438737,-99.123438026388797 26.520451579672599,-99.126618350727256 26.523927276756307,-99.127319211298612 26.524693229780183,-99.127781999999996 26.525199,-99.128040494672888 26.525242991472329,-99.131554903978838 26.52584108518932,-99.136510932879688 26.526684518463242,-99.143658999999985 26.527901,-99.157083944984777 26.532657279516766,-99.166741999999985 26.536078999999997,-99.170704 26.540316,-99.171403999999995 26.549848,-99.167459686682065 26.559874693319248,-99.167410000000004 26.560001,-99.168618869529794 26.566870928153797,-99.16946034016965 26.571652951934592,-99.178064000000006 26.620546999999998,-99.200522000000007 26.656442999999999,-99.209947999999997 26.693937999999999,-99.208906999999982 26.724761,-99.240022999999979 26.745850999999998,-99.242444000000006 26.788262,-99.243132825912184 26.78921716914337,-99.262208 26.815667999999999,-99.268613000000002 26.843212999999999,-99.274832961326339 26.850997131057774,-99.280470999999991 26.858053000000002,-99.295146000000003 26.86544,-99.316753000000006 26.865831,-99.328801222539823 26.879647723469141,-99.328900000000004 26.879760999999998,-99.328852280051947 26.879943529980665,-99.328653604395157 26.88070346927794,-99.326247644284351 26.88990632616278,-99.321819000000005 26.906846000000002,-99.324684000000005 26.915973,-99.337297000000007 26.922758999999999,-99.361143999999996 26.928920999999999,-99.367053999999996 26.929033999999998,-99.379148999999998 26.934489999999997,-99.388253000000006 26.944216999999998,-99.393748000000002 26.960730000000002,-99.390189000000007 26.966348,-99.377312000000003 26.973818999999999,-99.376593 26.977716999999998,-99.378434999999996 26.980034,-99.385448615007036 26.981891053234623,-99.387366999999998 26.982399,-99.403694000000002 26.997355999999996,-99.407320999999996 27.005808999999999,-99.415475999999998 27.017239999999997,-99.420446999999996 27.016567999999999,-99.429379999999981 27.010833000000002,-99.432154999999995 27.010698999999995,-99.438721 27.01463,-99.445682828533535 27.022104842939122,-99.446523999999997 27.023008,-99.446589518313687 27.0234513503828,-99.446929167151652 27.025749691622671,-99.446969999999979 27.026026000000002,-99.446787747918748 27.026353589968604,-99.444062000000002 27.031253,-99.443973 27.036457999999996,-99.447729715193731 27.048260380671568,-99.452315999999996 27.062668999999996,-99.450282 27.067705,-99.439210578806851 27.075275465844946,-99.434470000000005 27.078517000000002,-99.429209 27.090981999999997,-99.430274999999995 27.094871999999999,-99.437646 27.100442,-99.442122999999995 27.106839,-99.441108999999997 27.110041999999996,-99.433369999999982 27.119218,-99.430581000000004 27.126611999999998,-99.431354999999996 27.13758,-99.438264999999987 27.144791999999995,-99.439971 27.151071999999996,-99.437950999999998 27.154121,-99.42998399999999 27.159148999999999,-99.426616441133064 27.174998569551711,-99.42634799999999 27.176261999999998,-99.426389092127664 27.176475083747437,-99.428025433058153 27.184960350328335,-99.432794999999999 27.209693,-99.441928000000004 27.217984999999995,-99.442101400955139 27.218265584747954,-99.445237999999989 27.223341,-99.443121431464945 27.230753685991843,-99.441406999999998 27.236757999999998,-99.441548999999995 27.249919999999999,-99.452207395848959 27.263806782859465,-99.452390999999992 27.264046,-99.452635348600225 27.264144272092288,-99.454218033886534 27.264780796280988,-99.462735864984637 27.268206496624611,-99.463308999999981 27.268436999999995,-99.463731957038988 27.268231398925973,-99.480688 27.259988999999997,-99.487909999999999 27.260721,-99.492407 27.264118,-99.496069429210138 27.270723950024919,-99.496615000000006 27.271707999999997,-99.496412995851571 27.272119287725655,-99.490870463706145 27.283404082904614,-99.487572835142558 27.290118173493504,-99.487513000000007 27.29024,-99.487552182270463 27.290674424182523,-99.487937000000002 27.294941,-99.493651777311726 27.30231355132117,-99.494603999999995 27.303542,-99.494999311050705 27.30367917804087,-99.501697839297435 27.306003653868146,-99.502036000000004 27.306121,-99.50260564894451 27.305998370990778,-99.511531000000005 27.304076999999999,-99.522353224815092 27.304131436852781,-99.523657999999983 27.304137999999995,-99.527521386758664 27.305370598210363,-99.529216737576675 27.305911493159478,-99.529653999999994 27.306051,-99.533911450776046 27.310119063512179,-99.536090758332008 27.312201427353024,-99.536443000000006 27.312538,-99.537771000000006 27.316072999999996,-99.53137599999998 27.323809,-99.52136037329538 27.324774325824137,-99.521259999999998 27.324784,-99.520793197388144 27.325167862222077,-99.515101491997058 27.329848278790703,-99.509737777509301 27.334258981108004,-99.504836999999995 27.338289,-99.507830999999996 27.348637,-99.507785758525344 27.353517859091919,-99.507778999999999 27.354247,-99.505884699626023 27.358359262089539,-99.503847413823948 27.362781925614613,-99.502763417847504 27.36513512979511,-99.502013099315448 27.366763966750881,-99.499076000000002 27.373139999999999,-99.492143999999996 27.380516999999998,-99.488110785984318 27.408328989964531,-99.487887470616073 27.409868914391197,-99.487633320470721 27.411621467383494,-99.487521 27.412396,-99.487591555014376 27.412624523015491,-99.489856740583164 27.419961308946796,-99.495313182879073 27.437634363915539,-99.495699000000002 27.438884000000002,-99.495681276107845 27.439260342274874,-99.495103999999998 27.451518,-99.489866073767956 27.458841813736395,-99.48498035355378 27.465673163249864,-99.484933241276849 27.465739036942171,-99.483818999999997 27.467296999999995,-99.483170086267094 27.470026063960816,-99.480813109028745 27.479938539705284,-99.480418999999998 27.481595999999996,-99.480219000000005 27.485795999999997,-99.483519 27.491095999999999,-99.494943552274876 27.498766770813134,-99.497518999999997 27.500495999999998,-99.501722168778215 27.500229993495459,-99.502529258718056 27.500178915086504,-99.513319999999993 27.499496,-99.516119999999361 27.498282666666942,-99.519319999999979 27.496896,-99.525819999999982 27.496696,-99.528319999999994 27.498895999999998,-99.525855930815595 27.516294999999385,-99.525849791073696 27.516338353233991,-99.525316190784537 27.520106149807926,-99.523876376642917 27.530272798702207,-99.522431296340699 27.540476632400054,-99.521918999999997 27.544094,-99.518818999999993 27.553194,-99.514319 27.556994,-99.511118999999994 27.564493999999996,-99.512219000000002 27.568093999999995,-99.515978000000004 27.572130999999999,-99.51621006287597 27.572263354504685,-99.516956092136581 27.572688844074506,-99.522907946893667 27.576083418863931,-99.530137999999994 27.580207,-99.536560517954015 27.595669560441458,-99.539721999999998 27.603280999999999,-99.549741780062789 27.610632655019803,-99.554405160028892 27.614054243170667,-99.554950000000005 27.614453999999999,-99.555217670042637 27.614437037021997,-99.556811999999994 27.614335999999998,-99.559466999999998 27.609075999999998,-99.562869000000006 27.607264,-99.580005999999997 27.602250999999995,-99.584175941853502 27.603675176957196,-99.584843000000006 27.603902999999999,-99.584876722760043 27.604178863233781,-99.585148000000004 27.606397999999999,-99.578360965974085 27.610254799100861,-99.578159999999983 27.610368999999999,-99.578158133228257 27.610639131051315,-99.578098999999995 27.619195999999999,-99.584782000000004 27.622006999999996,-99.591372000000007 27.627464,-99.592626330929548 27.632690692534315,-99.594037999999998 27.638573,-99.596231000000003 27.639857999999997,-99.603532999999999 27.641991999999998,-99.612907806834755 27.638651258855042,-99.615318942915422 27.637792043123689,-99.624515000000002 27.634515,-99.625321999999997 27.631136999999999,-99.638929000000005 27.626757999999999,-99.654323999999988 27.629615999999999,-99.665948 27.635967999999998,-99.665422000000007 27.640274999999999,-99.660175716081199 27.644678795569117,-99.659499999999994 27.645246,-99.659300532255756 27.646059100049566,-99.658294999999995 27.650158,-99.661845 27.655753,-99.668942 27.659973999999998,-99.672015651285406 27.660163655087903,-99.685812999999982 27.661014999999999,-99.699355999999995 27.655417,-99.704600999999997 27.654953999999996,-99.711511000000002 27.658365,-99.721518999999986 27.666154999999996,-99.723715999999996 27.673328,-99.727277746539684 27.678262316066267,-99.732288643517421 27.685204233237535,-99.732448000000005 27.685424999999999,-99.732610774853441 27.685596448480599,-99.757538999999994 27.711853,-99.758533999999997 27.717071,-99.770740000000004 27.732133999999999,-99.774900999999986 27.73354,-99.785365999999996 27.730354999999999,-99.788844999999981 27.730718,-99.796341999999981 27.735586,-99.801651000000007 27.741771,-99.805670000000006 27.758687999999999,-99.813085999999998 27.773952,-99.817390803736785 27.775433523363962,-99.819091999999998 27.776019000000002,-99.822192999999999 27.766855,-99.825793000000004 27.764373999999997,-99.835127 27.762881,-99.841707999999997 27.766463999999999,-99.843346625073153 27.773142384459568,-99.844736999999981 27.778808999999999,-99.849281022020321 27.790032142335203,-99.850876999999997 27.793973999999999,-99.857944055165163 27.794214491272228,-99.870065999999994 27.794626999999998,-99.877441689362087 27.799278597548025,-99.877677000000006 27.799427,-99.877679299916537 27.799779028327777,-99.877693551047656 27.801960325692491,-99.877840000000006 27.824375999999997,-99.876677795668783 27.832975173255242,-99.876002999999997 27.837968,-99.877201999999997 27.842179000000002,-99.882014999999996 27.850391999999996,-99.89364999999998 27.856193,-99.901486000000006 27.864162,-99.904385000000005 27.875283999999997,-99.901231999999993 27.884405999999995,-99.894091000000003 27.892949999999999,-99.893456 27.899208000000002,-99.895827999999995 27.904177999999998,-99.900080000000003 27.912141999999999,-99.905861237749605 27.914081496997749,-99.917461000000003 27.917973,-99.925935042520848 27.927688375003317,-99.93714199999998 27.940536999999999,-99.938541 27.954059,-99.932160999999979 27.96771,-99.931811999999994 27.980967,-99.962768999999994 27.983536,-99.984922999999995 27.990729000000002,-99.991446999999994 27.99456,-99.998749958954292 28.00705628754028,-100.000278657311796 28.009672084008166,-100.008630999999994 28.023963999999999,-100.012838999999985 28.037203000000002,-100.014975394500183 28.048814882934586,-100.016570970484992 28.057487270710915,-100.017914000000005 28.064786999999999,-100.028724999999994 28.073118,-100.046108000000004 28.079067999999999,-100.053122999999985 28.08473,-100.056983000000002 28.094207,-100.05559599999998 28.101140999999998,-100.056492999999989 28.104185999999995,-100.064147158981442 28.110644603904401,-100.067651999999995 28.113602,-100.075474 28.124881999999999,-100.083393 28.144034999999999,-100.090288999999984 28.148312999999998,-100.119627999999992 28.155588000000002,-100.12658652988236 28.159659080291213,-100.141098 28.168149,-100.159890345070792 28.168159605160877,-100.160589999999999 28.16816,-100.1644540887186 28.169825181963088,-100.168437999999995 28.171541999999999,-100.173949604741296 28.178834844700365,-100.174413 28.179448,-100.17569869464343 28.180169771489844,-100.185693999999998 28.185780999999999,-100.195825662217615 28.189941498404405,-100.196499000000003 28.190218000000002,-100.19841872968675 28.190245400986015,-100.202449991411854 28.190302940621361,-100.208059000000006 28.190382999999997,-100.21208061434919 28.196473071951928,-100.212104999999994 28.196509999999996,-100.212111438897693 28.196576571978802,-100.212136678181992 28.196837521783539,-100.213449999999995 28.210415999999999,-100.217564999999993 28.226934,-100.220284000000007 28.232209999999995,-100.22363 28.235223999999995,-100.227575000000002 28.235856999999999,-100.246200000000002 28.234092,-100.251633999999996 28.236177,-100.261135590980771 28.244561246718906,-100.267604000000006 28.250268999999999,-100.280518 28.267969,-100.289383999999998 28.273491,-100.293468000000004 28.278475,-100.294296000000003 28.284381,-100.287553999999986 28.301093000000002,-100.286470999999992 28.312295999999996,-100.288638999999989 28.316977999999995,-100.314198000000005 28.345859,-100.317245999999997 28.357382,-100.320392999999996 28.362116999999998,-100.341869000000003 28.384952999999996,-100.344399999999979 28.389662,-100.34934096344108 28.4019924953441,-100.349585999999988 28.402603999999997,-100.349394820211828 28.402858691740477,-100.345766407828719 28.407692501181913,-100.343945000000005 28.410119000000002,-100.337058999999996 28.427150999999995,-100.336185999999998 28.430180999999997,-100.337474638078888 28.440402915586755,-100.337648113426326 28.441778981052359,-100.337796999999995 28.442959999999999,-100.338752462381066 28.444650728533539,-100.341532999999998 28.449570999999995,-100.350785999999999 28.459246,-100.353875644654778 28.461269551534915,-100.357498000000007 28.463642,-100.35795880406117 28.464220845064407,-100.358193534884933 28.464515705266944,-100.367961112704847 28.47678537623738,-100.368288000000007 28.477195999999999,-100.368051363629903 28.477598261305609,-100.365982000000002 28.481116,-100.356642877924983 28.482149981508559,-100.352234999999979 28.482638,-100.344180999999992 28.486249,-100.337140000000005 28.491728999999999,-100.33381399999999 28.499251999999998,-100.338517999999993 28.501833,-100.362147999999991 28.508399,-100.379079000000004 28.511638999999999,-100.388859999999994 28.515747999999995,-100.405057999999983 28.535779999999999,-100.411413999999994 28.551898999999999,-100.40430324537553 28.563833042228197,-100.397270000000006 28.575637,-100.396799999999999 28.580400999999998,-100.398385000000005 28.584883999999999,-100.403243426032972 28.586668145075244,-100.425819035320046 28.594958517689108,-100.429856 28.596440999999995,-100.431892715674053 28.597943579291371,-100.447320000000005 28.609324999999998,-100.448648000000006 28.616773999999999,-100.447280739633683 28.625703494601449,-100.445528999999993 28.637143999999996,-100.44560646842767 28.637394606891831,-100.447014715516048 28.641950223113035,-100.447091 28.642196999999999,-100.447325484276334 28.642184618041064,-100.447599559425257 28.642170145483281,-100.456522261830528 28.641698981546444,-100.462866000000005 28.641363999999999,-100.474494000000007 28.647071,-100.479445543783555 28.654922981332383,-100.479495071011698 28.655001519842354,-100.479635999999999 28.655225000000002,-100.481416494659797 28.655591917738484,-100.492493911747701 28.657874710783531,-100.493322226012125 28.658045406716248,-100.495863 28.658568999999996,-100.496129521434568 28.658770241190073,-100.500353999999987 28.661960000000004,-100.502122125123847 28.667202406240314,-100.505211969634487 28.676363647108229,-100.509438609439442 28.688895431533517,-100.510054999999994 28.690722999999995,-100.510127443340068 28.69126843161186,-100.510538898440004 28.694366309458967,-100.51077198772829 28.696121257064849,-100.511998000000006 28.705352,-100.511351260759739 28.706743032691019,-100.510646631791559 28.708258576930099,-100.509872363628347 28.709923903942272,-100.509406561254394 28.710925770365975,-100.508636802312012 28.712581398765199,-100.507069450532313 28.715952521820881,-100.506701000000007 28.716745000000003,-100.506745928327234 28.717920131927187,-100.506786973330165 28.718993692782757,-100.507152057645129 28.728542729239727,-100.507513721430456 28.738002299344281,-100.507590113222051 28.740000380261673,-100.507613000000006 28.740599,-100.507694735304739 28.740708529390524,-100.51442566685138 28.749728313832868,-100.519226000000003 28.756160999999999,-100.533017 28.763279999999998,-100.537772000000004 28.780775999999996,-100.534846642084489 28.786410367511117,-100.532431000000003 28.791062999999998,-100.535830000000004 28.805887999999999,-100.546431879116881 28.824270186264151,-100.546968759195195 28.825201061771438,-100.547323999999989 28.825817,-100.548186025546428 28.826178082695296,-100.553129999999982 28.828249,-100.561442999999997 28.829174000000002,-100.570509999999999 28.826317,-100.574698999999995 28.828786999999998,-100.576846000000003 28.836168000000004,-100.572991999999999 28.848463999999996,-100.580501999999996 28.856007999999999,-100.591040000000007 28.863054000000002,-100.598061272695219 28.874286065303028,-100.598877000000002 28.875590999999996,-100.602654 28.887660000000004,-100.602394435759351 28.893839359355621,-100.602053999999995 28.901944,-100.615584686670886 28.902906942475386,-100.627205999999987 28.903734,-100.631611000000007 28.902838999999997,-100.633502414453218 28.905240591668694,-100.640568000000002 28.914211999999999,-100.639169999999979 28.916288999999999,-100.638857000000002 28.927621999999996,-100.651511999999997 28.943431999999998,-100.64789859018552 28.954344193790252,-100.646992999999995 28.957078999999997,-100.646600907687997 28.967547400926616,-100.64647463428885 28.970918751315974,-100.645893999999998 28.986421,-100.647406325643686 28.99295674936236,-100.647835962928511 28.994813493392339,-100.648681241069511 28.998466493719445,-100.65094599999999 29.008254000000004,-100.653757999999996 29.015356,-100.656109999999998 29.017223999999999,-100.660207999999997 29.031497000000002,-100.663212 29.048041999999995,-100.662508000000003 29.058106999999996,-100.664064999999994 29.073205999999999,-100.666359117032755 29.07896154562156,-100.668483863047285 29.084292168447689,-100.674655999999999 29.099777000000003,-100.684472 29.110657,-100.692326999999992 29.115227999999995,-100.70996599999998 29.119684000000003,-100.727461999999989 29.129122999999996,-100.737795000000006 29.139078999999999,-100.739115999999996 29.141658,-100.737590999999995 29.147406999999998,-100.739681000000004 29.150486000000004,-100.746139999999997 29.154149000000004,-100.752330696165359 29.155516457617566,-100.759726 29.157149999999998,-100.76337082361762 29.160348915845475,-100.772648999999987 29.168492,-100.775904999999995 29.173344,-100.772154104961558 29.17809434863841,-100.766030999999998 29.185848999999997,-100.767059000000003 29.195287,-100.768348510582513 29.197581465531123,-100.775064591152173 29.209531592641572,-100.785521000000003 29.228136999999997,-100.791371999999996 29.225944999999999,-100.795681000000002 29.227729999999998,-100.79704599999998 29.235585999999998,-100.795234211471239 29.241421249558652,-100.795010188783237 29.242142762180318,-100.794911999999997 29.242459,-100.795310930062826 29.24310735172228,-100.797670999999994 29.246943000000002,-100.805332448258866 29.251327106905226,-100.815767091023289 29.257298117587734,-100.823532999999998 29.261742000000002,-100.834039999999987 29.261399999999998,-100.839016 29.263259,-100.841581161127962 29.265429071012271,-100.848663999999999 29.271420999999997,-100.856469000000004 29.275663999999999,-100.864659000000003 29.276076,-100.874739696506666 29.279181633366278,-100.876048999999995 29.279585,-100.876625364221198 29.280115401513385,-100.878513170701353 29.281852663087207,-100.878882999999988 29.282193000000003,-100.879105716359817 29.283377353454046,-100.880504361011205 29.290815018226784,-100.882051999999987 29.299044999999996,-100.886842 29.307848,-100.895590728097048 29.309871687341737,-100.904835000000006 29.31201,-100.906461302333199 29.313095095005444,-100.914770068080642 29.318638836799305,-100.916744062319239 29.319955917421627,-100.92203960930695 29.323489191321695,-100.922232587658954 29.323617949573855,-100.924041970395777 29.324825198761538,-100.926468967605715 29.326444530233299,-100.926628772437255 29.326551154580446,-100.926677999999981 29.326584,-100.92692137104487 29.32669794102517,-100.927819078352698 29.327118228044156,-100.92782883100196 29.32712279402223,-100.930446231588661 29.328348203997709,-100.940614999999994 29.333109,-100.941560773525751 29.336361493535293,-100.943196 29.341985,-100.945807189162338 29.344363370184055,-100.948971999999998 29.347245999999998,-100.95603875001359 29.347290187325033,-100.964324999999988 29.347342,-100.971743000000004 29.351370999999997,-100.972915999999998 29.354545000000005,-100.995606999999993 29.363403000000005,-101.004206999999994 29.364771999999999,-101.010614000000004 29.368669,-101.010768998319776 29.36895846820963,-101.014103165002794 29.375185214807839,-101.024016000000003 29.393697999999997,-101.036603999999997 29.406107999999996,-101.038600000000002 29.410713999999999,-101.037642000000005 29.414681000000002,-101.043363999999997 29.42988,-101.056956999999983 29.440773,-101.06011415724447 29.458454662113137,-101.060150999999991 29.458660999999999,-101.063843258270609 29.460131584976065,-101.087148999999997 29.469414,-101.103699000000006 29.470549999999999,-101.115253999999993 29.468458999999996,-101.130037999999985 29.47842,-101.137502999999995 29.473541999999998,-101.144336999999993 29.473246,-101.151876999999999 29.477004999999998,-101.163854997623304 29.493316894569897,-101.168923969306405 29.50021992913986,-101.171662999999995 29.503950000000003,-101.173821000000004 29.514566000000002,-101.192719999999994 29.520285,-101.227418999999998 29.522349999999996,-101.235274999999987 29.524854,-101.254895000000005 29.520341999999996,-101.260836999999981 29.529933,-101.261174999999994 29.536777,-101.252755240314642 29.553001111955531,-101.244355179006547 29.569187266927752,-101.24384013276574 29.574337712700849,-101.242022713082847 29.59251185083151,-101.251352546644355 29.604174135250076,-101.252152766968493 29.604494220898598,-101.259127443101093 29.607284069726152,-101.265347312053251 29.607284069726152,-101.274677145614746 29.602619152945408,-101.277624046978673 29.599940160672645,-101.283229510623855 29.594844301688571,-101.297224215766221 29.587069435365102,-101.30733154801338 29.587846934050759,-101.312894947857018 29.594028488101586,-101.314328915651188 29.595621785307589,-101.313192213312618 29.602947206633093,-101.31110792457288 29.616379301091623,-101.30733154801338 29.640715970810437,-101.311218981175116 29.64849082206727,-101.316661381574889 29.655488204771718,-101.325213716450733 29.657820655628775,-101.350093282659174 29.654710721152696,-101.353062961800887 29.655502634567405,-101.361755552011104 29.657820655628775,-101.364595571422868 29.66106639883844,-101.367197952410862 29.664040554714198,-101.371300910394638 29.676075888592084,-101.372874548462477 29.680691889931673,-101.378860251896057 29.698249939417508,-101.396947999999995 29.713947,-101.398362000000006 29.717000000000002,-101.396293999999997 29.727055,-101.397008999999997 29.733962999999999,-101.400635999999992 29.738078999999999,-101.410024000000007 29.741498,-101.415583999999996 29.746534,-101.415509877418046 29.750619769974676,-101.415402087456428 29.756561346443686,-101.424731921017937 29.758116313681725,-101.430388403484372 29.756500180308258,-101.441059122217254 29.75345141196761,-101.446501522617027 29.755006409338922,-101.451652003139955 29.758440048234313,-101.453498905321467 29.759671311053037,-101.455223999999987 29.771874,-101.467493655663716 29.779885945414083,-101.475268506920557 29.780663444099741,-101.503223000000006 29.764581999999997,-101.522695143280473 29.759671311053037,-101.526552276219391 29.761451532447605,-101.532802460460985 29.764336242900427,-101.53746737724174 29.782995910023434,-101.53804719015173 29.783865628452077,-101.546797210803248 29.796990645299058,-101.56156944476453 29.794658179375361,-101.572592853930203 29.778735448896484,-101.575564187573463 29.774443514881042,-101.582561562744587 29.771333610538232,-101.598573641854728 29.773657690388074,-101.603680999999995 29.774398999999999,-101.607256216824311 29.773863608191139,-101.625957999999997 29.771063000000002,-101.630319 29.768729,-101.632632680380155 29.763891873249722,-101.63512799999998 29.758675,-101.646417999999997 29.754303999999998,-101.652400999999983 29.758794999999996,-101.65328091488901 29.761368862201802,-101.654578 29.765162999999998,-101.662452999999985 29.77128,-101.689992000000004 29.771212999999996,-101.706636000000003 29.762736999999998,-101.714223999999987 29.767659999999999,-101.735201999999987 29.771591999999998,-101.754322999999999 29.777661999999999,-101.760919284561496 29.782457957985276,-101.763273999999981 29.784169999999996,-101.776796131253491 29.789191504347542,-101.777161000000007 29.789327,-101.777360180704861 29.789301830227139,-101.785668 29.788251999999996,-101.791002820423358 29.783037559970925,-101.796869557069002 29.782618516634159,-101.806507764952315 29.786389987871868,-101.809441149516488 29.790161459109573,-101.813855730843954 29.79253854440006,-101.814888826583982 29.793094827432377,-101.818909248247508 29.792167038125061,-101.830044431332681 29.789597381341238,-101.831231874027822 29.789323356194672,-101.852603555202364 29.801894932400803,-101.862241763085706 29.800218726571018,-101.866487373818487 29.79821962567333,-101.875399999999985 29.794022999999999,-101.878152615094365 29.794637055896249,-101.892739000000006 29.797891,-101.912406000000004 29.797849999999997,-101.917556726002999 29.792675767854249,-101.917942403066036 29.792482929945557,-101.922585359733716 29.790161459109573,-101.929709258872364 29.789323356194672,-101.932572380438657 29.791613852338052,-101.938090312383324 29.796028195755184,-101.946471365894325 29.797704401584976,-101.952535620779244 29.796990962273608,-101.953595265032959 29.796866298670082,-101.959462001678588 29.799380623656123,-101.965427194081556 29.806464275410743,-101.966166845299441 29.8073426094683,-101.970357372054934 29.81027599403247,-101.974547898810414 29.81027599403247,-101.982165144097308 29.804870201524977,-101.987538526473998 29.801056829485908,-101.993568005272067 29.802652866652032,-102.001318622543394 29.804704498914084,-102.001786318660763 29.804828300723614,-102.021918999999997 29.802490999999996,-102.032489182844799 29.803756293694118,-102.034758999999994 29.804027999999999,-102.039012999999997 29.802655,-102.041088000000002 29.799609999999998,-102.038411999999994 29.792831999999997,-102.039226999999997 29.790977000000002,-102.041308736499772 29.78984019520162,-102.048982832584102 29.785649487466547,-102.050044 29.785069999999997,-102.053123037129694 29.785312127485501,-102.073645999999982 29.786926,-102.076299925656656 29.790865308882584,-102.077348 29.792421,-102.084438999999989 29.794962000000002,-102.091420021879856 29.792967151942147,-102.091815999999994 29.792853999999995,-102.098789196918744 29.792718427915432,-102.115682000000007 29.792389999999997,-102.142325999999997 29.802854,-102.159600999999995 29.814355999999997,-102.161673999999991 29.819486999999999,-102.181894 29.846033999999996,-102.182859740836193 29.846584944255238,-102.18326563789924 29.846816503951921,-102.184058205433814 29.847268654791659,-102.186149999999998 29.848461999999998,-102.187393471220204 29.848699156882201,-102.188384798592551 29.848888224473839,-102.188671999999997 29.848942999999998,-102.189026115519184 29.848805138880103,-102.189342793483831 29.848681852617602,-102.205381000000003 29.842438,-102.216284162639099 29.842976962035557,-102.227553 29.843533999999995,-102.261388999999994 29.853283,-102.264041000000006 29.855964,-102.261776999999995 29.864001999999999,-102.264954000000003 29.867805999999998,-102.268816999999984 29.867990999999996,-102.276754999999994 29.862977999999998,-102.281249000000003 29.863116999999999,-102.297330999999986 29.875194,-102.301381000000006 29.877673999999995,-102.315388999999996 29.879919999999998,-102.320618999999994 29.878979999999995,-102.320667125014978 29.878900015386019,-102.320698585434869 29.878847727619664,-102.324634000000003 29.872306999999996,-102.333383999999995 29.868046,-102.341032999999996 29.869305000000004,-102.349861000000004 29.862316999999997,-102.361383773688715 29.849029038788231,-102.364542 29.845386999999995,-102.363642671554629 29.839963091609825,-102.362514000000004 29.833155999999995,-102.369522000000003 29.820395,-102.377253999999994 29.800163,-102.377312999999987 29.789971,-102.385270832882995 29.770348713937391,-102.386677616883858 29.766879890389689,-102.391739211722737 29.765814289574276,-102.392906191083 29.765568609270456,-102.402175111261812 29.766775086101941,-102.412062000000006 29.768061999999997,-102.433301 29.776608,-102.460890018677347 29.77909171043791,-102.468946430597143 29.779816991558327,-102.469957868267898 29.780012383523857,-102.481595292961757 29.782260529257879,-102.490330613701886 29.783948039559665,-102.492674483475767 29.783853017496881,-102.508312776666344 29.783219030534834,-102.508509848222559 29.783087649355906,-102.512686811979108 29.780303003853621,-102.512942156326147 29.774953626291172,-102.513380999999995 29.76576,-102.526400256170959 29.758693706517125,-102.535832205630967 29.753574449155192,-102.539417050278132 29.751628749336795,-102.545492105079163 29.750899740311965,-102.551081152293932 29.752357753652575,-102.556670813570321 29.757783010503054,-102.559343229460382 29.760376824671383,-102.565661280990938 29.761591836573395,-102.572255912443325 29.757095496286663,-102.57574472309652 29.754716761401177,-102.57635337725236 29.754301769870359,-102.585948674897452 29.751239442391949,-102.587774480184081 29.750656738873374,-102.597160000000002 29.751608,-102.612879000000007 29.748181999999996,-102.622534000000002 29.736632,-102.630150999999984 29.734314999999999,-102.64566499999998 29.733910000000002,-102.661251958261417 29.736122779697027,-102.66726872436665 29.739732837494621,-102.670971460158171 29.741954477821469,-102.677191937153026 29.738261067251376,-102.678467215476147 29.736427653943998,-102.6852530581628 29.72667193704833,-102.688163999999986 29.722486999999997,-102.690237999999979 29.707481999999999,-102.695507594622001 29.699754690880447,-102.698346999999984 29.695590999999997,-102.699316999999994 29.685029,-102.693466 29.676507,-102.697798916570122 29.672550546488289,-102.711337549653422 29.6601882100483,-102.724231000000003 29.648415000000004,-102.736947999999984 29.641537999999997,-102.742030999999997 29.632142000000002,-102.74048425929719 29.62775763619268,-102.738427999999999 29.621928999999998,-102.739991000000003 29.599041,-102.746201999999997 29.592875000000003,-102.757065999999995 29.597798999999998,-102.761810999999994 29.598396999999999,-102.768340999999992 29.594733999999999,-102.766929734823805 29.591197739636346,-102.762241000000003 29.579448999999997,-102.766124000000005 29.572347999999998,-102.768792219428661 29.572598581791439,-102.773961 29.573084,-102.776343296957975 29.562015327831393,-102.777530999999996 29.556497,-102.77572499999998 29.552188999999995,-102.771428999999998 29.548545999999998,-102.79216136048494 29.533953841063816,-102.807296321997939 29.523301326891541,-102.808565712504773 29.522407885546979,-102.808691999999979 29.522319000000003,-102.808681334358837 29.522097795383676,-102.808577631171161 29.519946998869006,-102.807327 29.494009000000002,-102.813953999999995 29.482805999999997,-102.814096473430837 29.482483316434472,-102.814383778328263 29.481832608662831,-102.822314986984821 29.463869465126457,-102.82537225240516 29.456945161410285,-102.827007089548758 29.453242470491304,-102.830969999999979 29.444267,-102.832538999999997 29.433109000000002,-102.830570753051859 29.427471931628293,-102.827354999999983 29.418261999999995,-102.825211066354328 29.40389180477127,-102.824564449740322 29.399557712155943,-102.831802092379931 29.389471209449322,-102.832669213065941 29.388262775214326,-102.84047188579207 29.377388836904565,-102.840778422425643 29.376961642203423,-102.841560439245441 29.370344567434586,-102.842457529677418 29.362753791491347,-102.843015380341285 29.358033509958585,-102.843020777220957 29.357987843989019,-102.861629999999991 29.351638999999995,-102.871857000000006 29.352092999999996,-102.876866000000007 29.354058999999996,-102.879534000000007 29.353326999999997,-102.883721999999992 29.348058999999999,-102.881857488401977 29.34363024944906,-102.879805000000005 29.338754999999999,-102.890489000000002 29.309498999999995,-102.88922161316745 29.299205074185686,-102.888328 29.291947,-102.891022000000007 29.287113000000002,-102.902604999999994 29.279440999999998,-102.90311226801488 29.27677066200776,-102.906295999999998 29.260010999999995,-102.903188999999983 29.254028999999999,-102.887901999999997 29.245611999999998,-102.881135 29.246022,-102.871347 29.241624999999999,-102.870795537867835 29.239589944231255,-102.86823682221636 29.230147538772215,-102.866845999999995 29.225014999999999,-102.878020000000006 29.214697999999999,-102.890063999999995 29.208813999999997,-102.899231 29.208863,-102.908656596973657 29.219194069767902,-102.908787000000004 29.219336999999996,-102.909098471667448 29.219296482603067,-102.910575587951939 29.219104333804097,-102.912131000000002 29.218902,-102.912650757516516 29.218481184275788,-102.915803535402844 29.215928573746115,-102.915865999999994 29.215877999999996,-102.915845750043147 29.215583596781208,-102.915608148145267 29.212129230727648,-102.915554 29.211341999999998,-102.915202931298523 29.21076702044931,-102.914525807203304 29.209658028088604,-102.912447999999998 29.206254999999999,-102.91453363655873 29.20019781620671,-102.917367531146525 29.191967513425833,-102.917805 29.190697,-102.922897000000006 29.192704085059273,-102.925481999999988 29.193722999999995,-102.932612000000006 29.194113000000005,-102.944911000000005 29.188819999999996,-102.947155999999993 29.180776999999999,-102.95089 29.176834999999997,-102.953474999999997 29.176307999999995,-102.977266 29.186226,-102.981742305424802 29.185103060319207,-102.989431999999994 29.183174,-102.98978258837333 29.182935350109393,-102.991725524817895 29.181612768970929,-102.994653 29.17962,-102.994906329461855 29.17910907354543,-102.99809599999999 29.172675999999996,-102.995688 29.161218999999999,-103.00243399999998 29.150260999999997,-103.0050672680876 29.149386797638936,-103.008362000000005 29.148292999999995,-103.010324999999995 29.137821999999996,-103.015028 29.125769999999996,-103.016945338084582 29.124525732477775,-103.024896087692937 29.119366048020158,-103.032982999999987 29.114118,-103.033302941039096 29.107355634443419,-103.034095953658579 29.105913798143234,-103.035682683422891 29.103028844591982,-103.04044215980575 29.099351067768175,-103.049126083320303 29.097714968852063,-103.055369601981923 29.096538655622457,-103.061557539334615 29.093936906572154,-103.06671291432167 29.091769303506382,-103.074407490743866 29.088534080562461,-103.076354546596249 29.085721668416742,-103.076667707631657 29.079576983004682,-103.076847 29.076059,-103.091926967746744 29.064237268591206,-103.100265999999991 29.057699999999997,-103.101359249603803 29.049403674773888,-103.102531654124817 29.040506667933872,-103.100975335951205 29.030701853789079,-103.100368259199087 29.026877266486249,-103.101607999999999 29.018122999999999,-103.107810999999998 29.013812,-103.117237999999986 29.000208999999998,-103.113922000000002 28.988546999999997,-103.115062910480361 28.985887850893185,-103.115327999999991 28.98527,-103.115773294260094 28.985147329619764,-103.126748000000006 28.982123999999995,-103.134930999999995 28.983525,-103.143274000000005 28.978073999999999,-103.156645999999995 28.972830999999999,-103.163865 28.972099,-103.165923000000006 28.974001999999999,-103.166234999999986 28.978836000000005,-103.174329720990258 28.980505274886987,-103.182663652045193 28.982223879127538,-103.194928465619142 28.984753100989199,-103.195705106971403 28.984913258196226,-103.207825367468075 28.987412670652219,-103.227338454203803 28.991436614861634,-103.227579101883322 28.991486240676846,-103.227800999999999 28.991532000000003,-103.228011662384262 28.991347921912006,-103.228737472224623 28.990713704806197,-103.239108999999985 28.981650999999996,-103.245121150470112 28.980239630911282,-103.2474464632202 28.980649978816817,-103.249155161185584 28.980951512720708,-103.25190330878641 28.984768118238364,-103.253285000000005 28.986686999999996,-103.26030800838916 28.989731411362609,-103.266003072061508 28.990206003834011,-103.270357000000004 28.988113999999996,-103.270725571428585 28.987466542857135,-103.27232562100086 28.984655789108508,-103.273357000000004 28.982844,-103.273647076585334 28.982817854078331,-103.281189929980513 28.982137982403092,-103.282424051550748 28.983865752282551,-103.284749352823042 28.987121173462914,-103.285935817906974 28.994002716014545,-103.289257951411443 28.999697788883793,-103.296614077237592 29.004443676810233,-103.302399476821606 29.006455988366771,-103.312987409437454 29.010138745081029,-103.315449022736544 29.011725838031147,-103.323993942362023 29.017235063099889,-103.331021803791103 29.021766184756,-103.332920159881368 29.026986669752301,-103.332783823258595 29.028827200289001,-103.332445567409962 29.033393619832523,-103.334818511373186 29.039800579109659,-103.341462759988346 29.041224342728523,-103.347869719265475 29.037427630547988,-103.350815999999995 29.028566999999999,-103.355428000000003 29.021529,-103.355590463621823 29.021464336016578,-103.361777791205157 29.019001647792766,-103.361998 29.018913999999999,-103.382125000000002 29.024248999999998,-103.386095607142025 29.025822707722725,-103.399799910010643 29.031254261761571,-103.402689272115182 29.032399429564627,-103.427474921518822 29.04222295692054,-103.427754276336316 29.042333676218103,-103.430481680804888 29.047455485683194,-103.434668000000002 29.055316999999995,-103.439550459717495 29.058547821156772,-103.449722032594721 29.065278554178825,-103.453029264571228 29.067467015663059,-103.457386392383299 29.068596640465525,-103.460381908910477 29.067681344107211,-103.463195887793432 29.066821517562918,-103.469166763983068 29.069242141692889,-103.471264639062966 29.073115142802621,-103.471299587628792 29.074897490538035,-103.471426016715213 29.08134526859719,-103.474814887995407 29.086670637304994,-103.484429000000006 29.094524,-103.49531945696836 29.1087608679228,-103.500928937954981 29.116094025764934,-103.503236 29.119109999999996,-103.506163032831353 29.119368513261239,-103.513192088151087 29.119989313955617,-103.524613000000002 29.120998,-103.524225442342612 29.124905426308125,-103.523383999999993 29.133389,-103.525026999999994 29.137353999999998,-103.539240927934515 29.144791265038371,-103.551909954693073 29.151420179312847,-103.558678999999984 29.154961999999998,-103.579462000000007 29.149964999999998,-103.592359999999999 29.150259999999999,-103.598960126757959 29.155019048637598,-103.606437999999997 29.160411,-103.607894091053709 29.162314354517299,-103.610539999999986 29.165772999999998,-103.624537921955991 29.163185608071569,-103.629433693714475 29.1622806680118,-103.638195234739996 29.160661174732635,-103.645634999999999 29.159286000000002,-103.653180000000006 29.162863999999995,-103.656807999999984 29.169098999999999,-103.660202999999996 29.170933999999999,-103.667724667668963 29.172878689431396,-103.669696386484915 29.173388467437,-103.688670000000002 29.178293999999998,-103.688830238367032 29.178273608722382,-103.690116124218548 29.178109972160996,-103.697314000000006 29.177194,-103.699305464408098 29.178139630948273,-103.713769999999997 29.185008,-103.724743000000004 29.191469999999999,-103.73193762439729 29.198544108660482,-103.742175000000003 29.20861,-103.750912464183997 29.219357091602017,-103.755265634411842 29.22471149629115,-103.75594348727293 29.225545256136954,-103.768569999999997 29.227360999999995,-103.777623396259372 29.232265264832392,-103.780085146052983 29.242351446713872,-103.780352362214714 29.243446273985068,-103.781659908552058 29.248803500057083,-103.789034484601103 29.257501704713096,-103.792700821322356 29.259284649416337,-103.795120675584712 29.260461427946989,-103.80876361213285 29.267096007175422,-103.816641821688407 29.27092719156084,-103.818617217853202 29.271599921312173,-103.838302999999982 29.278303999999999,-103.854966933859771 29.281484400071786,-103.856892999999999 29.281852,-103.880606 29.284961999999997,-103.896615144488038 29.284634799403065,-103.910231251371073 29.284356508561018,-103.916269383748329 29.284233099060927,-103.91710599999999 29.284215999999997,-103.91776286712728 29.284516760669142,-103.918698764585329 29.284945281345607,-103.918909999999997 29.285042,-103.918905327188597 29.285488562070029,-103.918900778480648 29.285923264066316,-103.918857000000003 29.290106999999995,-103.924975999999987 29.293913000000003,-103.943697999999983 29.294945999999996,-103.965795999999983 29.298586999999998,-103.975234999999998 29.296016999999999,-103.983459597310627 29.299165977024781,-103.998789342279238 29.305035323921498,-104.012357062242771 29.310230038851625,-104.018558084893087 29.312604243583909,-104.038281999999995 29.320155999999997,-104.047006656168747 29.325575022319434,-104.055595999999994 29.330909999999996,-104.056467149578211 29.334496091467582,-104.057243999999997 29.337694000000003,-104.068917843404819 29.342306667996301,-104.075923999999986 29.345075,-104.082149999999999 29.345922999999996,-104.091021999999995 29.353691999999995,-104.093326000000005 29.359926000000002,-104.098377999999983 29.366755999999999,-104.106466999999981 29.373127,-104.122882935723638 29.379859019095424,-104.125474999999994 29.380922000000002,-104.132831527553293 29.381873417846823,-104.136236243923818 29.382313748953422,-104.143691999999987 29.383277999999997,-104.1467332019064 29.385415391432094,-104.148888696362661 29.386930297552944,-104.151718842950046 29.388919356896473,-104.157422367906094 29.392927859373117,-104.166562999999996 29.399352,-104.180069999999986 29.412763999999999,-104.181273000000004 29.426265,-104.182051945448123 29.427144615030393,-104.195989999999995 29.442884000000003,-104.208194000000006 29.448201,-104.212529153241931 29.452438774515159,-104.213947876032222 29.456222068892099,-104.21370324326665 29.462011765001886,-104.213238514637069 29.473010445135856,-104.218538498245692 29.476600759818915,-104.220568643482977 29.477976020723915,-104.227547514371139 29.480496161448666,-104.229081071868791 29.481049944541308,-104.233824999999996 29.486544999999996,-104.233486999999997 29.492733999999995,-104.235847000000007 29.496744,-104.238313284768751 29.498023301020304,-104.246870092866089 29.50246185307569,-104.254473621536903 29.506405923975272,-104.260293266516271 29.50942466611497,-104.26168489846043 29.511073814728768,-104.264155000000002 29.514001,-104.271046277211582 29.51559593462645,-104.274575430964404 29.516412730896519,-104.293481117436073 29.520788310787555,-104.306646311930564 29.523835296697214,-104.308812834933008 29.524336722260216,-104.311570786550874 29.52540915148019,-104.318073989469781 29.527937921306467,-104.320711000000003 29.526326414398167,-104.326090351466348 29.523039031981277,-104.327483437129615 29.522187701603762,-104.334811000000002 29.519462999999998,-104.338113000000007 29.519967,-104.353150868059984 29.530471948300562,-104.369085650105362 29.541603450512167,-104.37074044290263 29.542759433043344,-104.371174999999994 29.543062999999997,-104.371454921502831 29.543072731712495,-104.371996954766473 29.543091575966443,-104.375901661123379 29.543227326450971,-104.381040999999996 29.543405999999997,-104.394588999999982 29.556087000000002,-104.394583075653415 29.556197720561212,-104.394577025204526 29.556310797858156,-104.394552364240909 29.55677168847231,-104.394503136758885 29.55769170460702,-104.394351 29.560534999999998,-104.39571704642303 29.563607040276512,-104.39636925661604 29.56507376640522,-104.397848504811165 29.56840038104859,-104.399550133070164 29.572227096202102,-104.399591 29.572319,-104.399981791506633 29.572551361916329,-104.452301000000006 29.60366,-104.466520000000003 29.609296,-104.483502 29.627740999999997,-104.486693164979016 29.629712817604439,-104.493658999999994 29.634016999999997,-104.503232654335022 29.63787621670291,-104.507568060311556 29.63962385355849,-104.51068846437596 29.64315687341222,-104.513969549435132 29.646871821353749,-104.51818442992635 29.651644041921735,-104.53325150001281 29.668703453669117,-104.535568725458901 29.671327089370656,-104.539761023554547 29.676073741438437,-104.540155514025756 29.678572204873163,-104.540559188867974 29.681128836544634,-104.537934834631798 29.684482179324355,-104.535770155740693 29.687248158943191,-104.536302269386866 29.690440851131939,-104.537991394981006 29.693268523024599,-104.544269904764263 29.703779029587054,-104.545505621253113 29.70584767433138,-104.552240999999995 29.717122999999997,-104.555600999999996 29.731220999999998,-104.554914970831931 29.738152096413199,-104.554660236582549 29.740725729903364,-104.557361904757329 29.745049367744027,-104.565950999999998 29.758794999999996,-104.565687999999994 29.770461999999998,-104.571279565966023 29.778773775962186,-104.586447318468245 29.801320404476364,-104.592472 29.810276000000002,-104.594023000000007 29.809220999999997,-104.599148999999997 29.811007,-104.610166000000007 29.819117999999996,-104.610205623651026 29.819231101342197,-104.610356932717465 29.819662996386217,-104.613081011852316 29.827438579869643,-104.615248982559379 29.833626813172685,-104.619039 29.844444999999997,-104.624350000000007 29.845221999999996,-104.629290740007292 29.852171499068138,-104.629713747950376 29.852766489555783,-104.630103000000005 29.853313999999997,-104.630111961493199 29.853664893019587,-104.630290851730521 29.860669455113747,-104.630338938536084 29.862552324858758,-104.630359999999996 29.863376999999996,-104.630588419131911 29.86393398222631,-104.633274999999998 29.870484999999999,-104.63616345635748 29.872800876528242,-104.645854762904364 29.880571071602066,-104.65678299999999 29.889332999999997,-104.658422170930251 29.891285606400047,-104.65864999999998 29.891556999999999,-104.658665881124946 29.891956296855856,-104.659041999999999 29.901413000000002,-104.661965712024596 29.903547518850328,-104.666224133352003 29.906656470935726,-104.672326999999996 29.911111999999996,-104.679772 29.924658999999998,-104.680850906343153 29.932111041680564,-104.684321999999995 29.956085999999999,-104.679660999999982 29.975271999999997,-104.680379178583593 29.977083,-104.685479 29.989943,-104.689640999999995 30.014949999999999,-104.693591999999995 30.019077000000003,-104.701242868742796 30.021046973658397,-104.702310999999995 30.021321999999998,-104.702447580064685 30.021555813412469,-104.7030116881661 30.022521518330599,-104.703997999999999 30.02421,-104.703882705803636 30.024660825787098,-104.703121214098445 30.027638426639932,-104.701101999999992 30.035533999999998,-104.706873999999999 30.050685,-104.706630611705322 30.051708605996314,-104.703831250551389 30.063481739403372,-104.703581999999997 30.06453,-104.699792220462342 30.065066336345112,-104.698848962238912 30.065199827927678,-104.698233000000002 30.065286999999998,-104.697787990647399 30.065651654776563,-104.69277799999999 30.069756999999996,-104.687313590037505 30.080921966773531,-104.685080632167896 30.08548438075637,-104.685002999999995 30.085643,-104.685554782105299 30.093963293324606,-104.685687 30.095957000000002,-104.686861171517634 30.098036494960315,-104.692093999999983 30.107303999999996,-104.693655813605488 30.119154117533654,-104.695366000000007 30.13213,-104.692122999999995 30.138662999999998,-104.690080458880587 30.149079251722458,-104.687506999999997 30.162202999999998,-104.687296000000003 30.179464000000003,-104.702787999999998 30.211735999999995,-104.707897632633845 30.218501061672534,-104.711235999999985 30.222920999999999,-104.713166 30.237956999999998,-104.722357186598074 30.248308653999679,-104.724410584274111 30.250621311025988,-104.72532436157617 30.251650460675243,-104.73347160041142 30.260826359409911,-104.733822000000004 30.261220999999999,-104.734059376092361 30.261231667834558,-104.737359999999995 30.261379999999996,-104.740448 30.259454000000002,-104.749663999999996 30.26126,-104.751566999999994 30.263643999999999,-104.754655151309422 30.272796681105522,-104.757893999999993 30.282395999999999,-104.761634 30.301147999999998,-104.779356000000007 30.313188,-104.790279572675416 30.323632238875831,-104.797291 30.330335999999999,-104.809794338457337 30.334925849121163,-104.810755485390246 30.338091979227556,-104.81211787963197 30.342579864771171,-104.813478341684558 30.347061385458392,-104.814379749150064 30.356375955470707,-104.814778573671205 30.360497153782266,-104.817595751374526 30.365914799658352,-104.824313633436745 30.370465624210016,-104.837492999999995 30.373909,-104.849824017938076 30.383147747051474,-104.859521 30.390413000000002,-104.857439999999997 30.408957,-104.852419999999995 30.418792,-104.861074000000002 30.428896999999996,-104.86474141169117 30.441297336779865,-104.865463293164552 30.443738179024677,-104.868454614768496 30.453852504447951,-104.869872 30.458644999999997,-104.86871099999999 30.463229999999996,-104.866118999999983 30.46479,-104.866094000000004 30.467379999999999,-104.870137228360662 30.483875070981508,-104.873288503237063 30.496731258693877,-104.873335488188886 30.496922942181985,-104.876786999999979 30.511004000000003,-104.878566789651714 30.514416830422793,-104.880108146577356 30.517372454871531,-104.88208573587417 30.521164575423189,-104.889375999999999 30.535143999999995,-104.890392881954469 30.540947215529823,-104.892228000000003 30.551419999999997,-104.895440150997459 30.560421421221292,-104.899000999999998 30.570399999999996,-104.909330873116318 30.584188648619556,-104.909747767935556 30.584745133303247,-104.918689620637082 30.596681007395873,-104.924796 30.604831999999998,-104.927016167130404 30.604700501077968,-104.934922481779466 30.604232215677584,-104.939873000000006 30.603939,-104.953391370086464 30.606003357240429,-104.967167000000003 30.608106999999997,-104.971627504383378 30.61006529240159,-104.972071 30.61026,-104.972794851616513 30.611297344530712,-104.980135850742045 30.621817657146149,-104.980290999999994 30.622039999999995,-104.982191641583043 30.62882153037463,-104.983981 30.635206,-104.985513855292751 30.652294791670293,-104.9863 30.661059000000002,-105.001239999999996 30.672583000000003,-105.002056999999994 30.680971999999997,-105.006614566555172 30.685839873047019,-105.006800999999996 30.686039,-105.007528432885024 30.6859080282745,-105.007959930235828 30.685830338698263,-105.020141999999993 30.683637,-105.035888798004834 30.686138071331488,-105.042930223005172 30.687256464175281,-105.044973600167665 30.685364445024991,-105.049769587265445 30.680923708581364,-105.049884734287517 30.6808170907847,-105.050688284649809 30.681171184306308,-105.054688384336032 30.682933873306403,-105.062333999999993 30.686302999999995,-105.062477471553692 30.692159292631892,-105.062625999999995 30.698222,-105.084504902225902 30.710918832086001,-105.098281999999998 30.718914000000002,-105.108075999999997 30.730049999999995,-105.110705999999993 30.737749999999995,-105.110681999999983 30.743366000000002,-105.113815999999986 30.746001,-105.119547104826722 30.748125675649064,-105.123265000000004 30.749504000000005,-105.140207000000004 30.752502,-105.152361999999982 30.751451999999997,-105.157640215066564 30.754007791997694,-105.16015278062757 30.757058756266805,-105.160271207191187 30.758203550015399,-105.161229588477383 30.767467931854636,-105.164076390339474 30.771453450048199,-105.164818961888187 30.77249304906519,-105.164868027003308 30.772491740665838,-105.170370203038615 30.772345016388588,-105.178279098267282 30.772134113115257,-105.183436 30.776644999999995,-105.185930999999997 30.784391999999997,-105.195143999999999 30.792138,-105.195988495740664 30.791702299374609,-105.203522176185984 30.787815448176353,-105.206160999999994 30.786453999999996,-105.212916518294108 30.785414778041517,-105.215967484302155 30.786491589369188,-105.216685356202021 30.789542555377231,-105.215888956387687 30.792967070566242,-105.214890669496626 30.797259699168034,-105.218659510882389 30.801566944478708,-105.222008841883465 30.801829013843189,-105.231580057313238 30.802577916338681,-105.238364256021057 30.803108747911697,-105.249792715464977 30.799033957074698,-105.255416052233826 30.797028969328842,-105.261224946310648 30.798054073736733,-105.261360733612406 30.798078036329223,-105.27276013440985 30.808707209360072,-105.27887297001547 30.814407016614517,-105.283004544196217 30.818259431108679,-105.287237620233441 30.822206489243744,-105.289317685342255 30.822206489243744,-105.295630129130572 30.822206489243744,-105.300778681061615 30.818848737795758,-105.303672944509898 30.816961174571279,-105.314862960890409 30.816961174571279,-105.316949659843416 30.82296045428,-105.31766045482081 30.825003996727105,-105.320108268786385 30.827451797139727,-105.322675464925993 30.828439452154051,-105.330102631610075 30.831296841312803,-105.347695000000002 30.838065,-105.353219571709047 30.842032277683753,-105.358103536812379 30.84553952616983,-105.359771122039604 30.846737044096301,-105.360672052753856 30.84738401593513,-105.36720323988537 30.847875849249167,-105.377416999999994 30.848644999999998,-105.387780492422849 30.851314552204784,-105.394242074789418 30.852979003795937,-105.396689881978517 30.855426817761515,-105.396237319899697 30.858492407966825,-105.394628539050657 30.86939005724841,-105.394248999999988 30.871960999999999,-105.395681745186749 30.87649980844607,-105.399608999999998 30.888940999999996,-105.399828021807267 30.889112280223049,-105.402824820246408 30.891455847338634,-105.413505 30.899808,-105.430088999999995 30.905791999999998,-105.44547819369545 30.915748838601054,-105.448693870984556 30.917829388134351,-105.452149246331331 30.920065022782566,-105.4597280766598 30.924968540917344,-105.469954295993247 30.931584924947412,-105.472488639470896 30.933224650164071,-105.476269681741059 30.935670991952524,-105.488027000000002 30.943277999999999,-105.495516502166126 30.9493992090351,-105.4968561000139 30.950494069316512,-105.497422439936116 30.953962910943051,-105.497963910074574 30.957279424722472,-105.502256687477072 30.962680017552614,-105.507558525965123 30.966493977230062,-105.533087999999992 30.984859,-105.541468808393844 30.984769556162778,-105.543675999999991 30.984745999999998,-105.557430349577899 30.990228688381027,-105.570063935625512 31.008532833135739,-105.575218046011173 31.016000355201566,-105.578084306850855 31.020153131231776,-105.578407858220658 31.020621907958049,-105.57911399999999 31.021644999999999,-105.580554486932016 31.024917232759986,-105.581339666024604 31.026700857930102,-105.581404000000006 31.026847000000004,-105.581361569643192 31.027041810483581,-105.581235244876609 31.027621805343596,-105.579823718925667 31.034102543987302,-105.579541999999989 31.035395999999995,-105.579765234089464 31.036249085539566,-105.579847333127134 31.036562825712579,-105.585323000000002 31.057487999999996,-105.592709163135751 31.0626132919136,-105.595921000000004 31.064841999999999,-105.598772999999994 31.074925999999998,-105.60333 31.082624999999997,-105.627348999999995 31.098545,-105.641890000000004 31.098321999999996,-105.646730999999988 31.113907999999999,-105.647031366805422 31.114192798578223,-105.648833999999994 31.115902000000002,-105.662869191777489 31.12063916934996,-105.673930643060629 31.124372639388369,-105.709491 31.136374999999997,-105.717005999999998 31.141438,-105.717653520299876 31.143411480377267,-105.719539999999995 31.149160999999996,-105.742677999999998 31.164897,-105.763531 31.164121000000005,-105.773256999999987 31.166896999999999,-105.774148788612138 31.168976038235485,-105.780021000000005 31.182666,-105.779878030009812 31.18682806893737,-105.779724999999999 31.191282999999995,-105.782894999999996 31.197562999999999,-105.790659746923424 31.200723362140888,-105.794386000000003 31.20224,-105.818835000000007 31.230680999999997,-105.825806058218902 31.23733997673531,-105.835722000000004 31.246811999999995,-105.851073632158958 31.265902599748792,-105.86604427072389 31.284519412988434,-105.869353000000004 31.288633999999998,-105.869862116496506 31.288859823963843,-105.876014999999981 31.291588999999995,-105.890871999999987 31.290013999999996,-105.895034999999993 31.290977999999999,-105.903460999999993 31.306768999999999,-105.908770999999987 31.312773999999997,-105.914613899052299 31.312859252963214,-105.93196564998388 31.313112430054005,-105.932552999999999 31.313120999999995,-105.933375187723101 31.313903465142818,-105.938451999999984 31.318735,-105.948091000000005 31.340069,-105.945903 31.352809999999998,-105.953942999999995 31.364749,-105.970101 31.365936999999995,-105.997579969562565 31.386863626037869,-106.00418474278807 31.391893495117941,-106.004925999999998 31.392457999999998,-106.006143777923754 31.392558937255828,-106.022866323827969 31.393945009265412,-106.080091217088636 31.398688175961098,-106.080258 31.398702,-106.080335505244278 31.398768097394733,-106.096409750603954 31.412476405141465,-106.100621535494 31.416068265421309,-106.102641400977276 31.417790830744398,-106.10264993125142 31.41779810546371,-106.106876999999997 31.421403000000005,-106.112168999999994 31.423577999999996,-106.132782000000006 31.425367000000005,-106.143572676566535 31.431101721097118,-106.158218000000005 31.438885000000003,-106.175305198785026 31.455910533348611,-106.175674999999998 31.456278999999995,-106.17677981450808 31.456634312625521,-106.182946895641223 31.45861766980741,-106.20560859826729 31.465905761156737,-106.205826999999999 31.465975999999998,-106.205998876707866 31.466165148890862,-106.212686244283063 31.473524541419071,-106.218538531147885 31.479964934554566,-106.218843000000007 31.480299999999996,-106.218893022078106 31.480498686193428,-106.223908999999992 31.500422,-106.236804000000006 31.513376,-106.242649095206133 31.530650094003715,-106.245874455325477 31.540182047193959,-106.246202999999994 31.541153,-106.246406806009631 31.541315626597875,-106.254779999999997 31.547997000000002,-106.280345588488331 31.561810530102129,-106.280548572587122 31.561920205925162,-106.280811 31.562061999999997,-106.287785 31.584444999999999,-106.292254713772053 31.594651759250389,-106.295141086978958 31.601242900860864,-106.303535999999994 31.620412999999996,-106.308594276522342 31.627497440426588,-106.330970256390501 31.658836434185069,-106.33473699999999 31.664111999999999,-106.338265602146336 31.671883697950737,-106.346992532059133 31.691104641686092,-106.349537999999995 31.696711,-106.357472918704602 31.702103016258707,-106.368303742040567 31.709462886938788,-106.370138999999995 31.710709999999999,-106.373839000000004 31.714809999999996,-106.378039 31.72831,-106.381039 31.732109999999999,-106.38595139194058 31.734759025425479,-106.394642915450532 31.739445961452269,-106.404086535473255 31.744538468290354,-106.417940000000002 31.752009,-106.421739999999616 31.752623660686648,-106.431540999999996 31.754208999999996,-106.434644983693715 31.755853956158482,-106.444263765046571 31.76095142933643,-106.451540999999992 31.764807999999999,-106.467641999999998 31.759607999999997,-106.470742 31.753508,-106.472156160715315 31.752506597443457,-106.47367184987327 31.751433300058491,-106.475016438009845 31.750481163584279,-106.475542000000004 31.750108999999998,-106.48261401078824 31.748321568701869,-106.484641999999994 31.747809000000004,-106.486162292248409 31.747994847970777,-106.488078402545341 31.748229082678503,-106.489542 31.748407999999998,-106.507341999999994 31.761208,-106.509102011460428 31.762827435120251,-106.511609062807707 31.765134242258316,-106.523643000000007 31.776206999999996,-106.528643000000002 31.781807,-106.528542999999999 31.783906999999996,-106.528542999999999 31.784407000000002,-106.527996999999999 31.786944999999999,-106.527623000000006 31.789119000000003,-106.527737999999985 31.789760999999995,-106.527942999999993 31.790507000000002,-106.530514999999994 31.792103,-106.532480000000007 31.791913999999998,-106.533 31.791829,-106.533043000000006 31.791906999999995,-106.534743000000006 31.796106999999999,-106.535154000000006 31.797088999999996,-106.535342999999997 31.797507,-106.535842999999986 31.798607,-106.542096999999998 31.802145999999997,-106.542143999999993 31.802106999999996,-106.544713999999999 31.804286999999999,-106.545344 31.805007,-106.546561898485905 31.806561850400339,-106.546615562760394 31.806630361790774,-106.547143999999989 31.807304999999996,-106.551861034152338 31.808599471053657,-106.558443999999994 31.810406,-106.560680020141419 31.810752754512048,-106.560847305685968 31.810778696593822,-106.562944999999999 31.811104,-106.56344399999999 31.812605999999995,-106.566844000000003 31.813305999999997,-106.569123704016391 31.811582321353455,-106.570943999999997 31.810205999999997,-106.577243999999993 31.810406,-106.581344 31.813905999999996,-106.582144 31.815505999999999,-106.588044999999994 31.822105999999998,-106.589044999999984 31.822705999999997,-106.593826000000007 31.824901,-106.602727000000002 31.825023999999996,-106.605266999999998 31.827911999999998,-106.603310537982878 31.834798487166189,-106.601945 31.839604999999999,-106.60204499999999 31.844404999999998,-106.605244999999982 31.845904999999998,-106.605845000000002 31.846304999999997,-106.614637000000002 31.846489999999996,-106.621857000000006 31.852854,-106.625763000000006 31.856276,-106.627808000000002 31.860592999999998,-106.629708357956787 31.861913746439043,-106.635925999999998 31.866235,-106.635879999999986 31.871514,-106.634872999999999 31.874477999999996,-106.630798999999996 31.879696999999997,-106.629271361585637 31.8835303997664,-106.629197000000005 31.883717000000004,-106.629948941610351 31.885072003811555,-106.630443527558498 31.88596325099838,-106.630530381920593 31.886119763139838,-106.630691999999982 31.886410999999999,-106.633926999999986 31.889183999999997,-106.635073463799642 31.889856364267651,-106.636317332120683 31.890585853164694,-106.638154 31.891662999999998,-106.638364165758659 31.89171923904625,-106.642899999999983 31.892932999999999,-106.645296000000002 31.894859,-106.645645999999999 31.895648999999995,-106.645478999999995 31.898669999999999,-106.640839999999997 31.904598,-106.63512839833713 31.908732779117901,-106.633668 31.90979,-106.633408736085542 31.909871832166754,-106.625946999999996 31.912227,-106.62344499999999 31.914034,-106.618336531290581 31.916262323146238,-106.614677480394235 31.917858407661853,-106.614345999999998 31.918002999999995,-106.611846 31.920003,-106.616063629645311 31.921863544491501,-106.623932999999994 31.925334999999997,-106.624022586877501 31.92530240401349,-106.628663000000003 31.923614,-106.629746999999995 31.926570000000002,-106.625321999999983 31.930053000000004,-106.622529 31.934863000000004,-106.622117000000003 31.936620999999999,-106.622377 31.940863,-106.623659000000004 31.945509999999995,-106.616135999999983 31.948439,-106.614701999999994 31.956,-106.617707999999993 31.956008,-106.622819000000007 31.952891,-106.625123000000002 31.954530999999999,-106.625534999999985 31.957475999999996,-106.624298999999993 31.961054,-106.620453999999981 31.963402999999996,-106.619370999999987 31.964777000000005,-106.618745000000004 31.966954999999995,-106.619568999999998 31.971578,-106.621872999999994 31.972933,-106.623215999999999 31.972909999999999,-106.626465999999994 31.97069,-106.630114000000006 31.971257999999999,-106.638186000000005 31.976819999999996,-106.639528999999996 31.980347999999996,-106.63649199999999 31.985719,-106.631181999999995 31.989809,-106.629494617643616 31.990072722748106,-106.628337072326531 31.990253636712819,-106.62822051159516 31.990271854111079,-106.626910882874782 31.990476537349483,-106.623567999999992 31.990998999999999,-106.619448000000006 31.994733,-106.618486000000004 32.000494999999994,-106.599096000000003 32.000731000000002,-106.598639000000006 32.000753999999993,-106.595332999999997 32.000777999999997,-106.587971999999979 32.000748999999999,-106.582805114232855 32.000751357586125,-106.566056000000003 32.000759000000002,-106.565141999999994 32.000736000000003,-106.564298514026802 32.00073927393025,-106.562131862706067 32.000747683631808,-106.55942760630407 32.000758180008894,-106.517043115844388 32.000922692365819,-106.411074999999983 32.001334,-106.409326934223301 32.001349629127162,-106.409108574114072 32.001351581443814,-106.394297999999992 32.001483999999998,-106.376861000000005 32.001171999999997,-106.32356972407176 32.001457096670784,-106.313306999999995 32.001511999999998,-106.205915000000005 32.001761999999999,-106.200699 32.001784999999998,-106.18183999999998 32.002049999999997,-106.125534000000002 32.002532999999993,-106.05045734599986 32.002412317859424,-105.998002999999997 32.002327999999999,-105.900599999999997 32.002099999999992,-105.886159000000006 32.001969999999993,-105.854061000000002 32.00235,-105.750527000000005 32.002206,-105.731362000000004 32.001564000000002,-105.563520259526442 32.001015604709174,-105.429281000000003 32.000577,-105.428582000000006 32.000599999999999,-105.427048999999997 32.000638000000002,-105.390395999999981 32.000607000000002,-105.340142768272344 32.000583616714373,-105.200871148394683 32.000518812363367,-105.153993999999997 32.000497000000003,-105.150310000000005 32.000497000000003,-105.14824 32.000484999999998,-105.132915999999994 32.000518,-105.131377 32.000523999999999,-105.118039999999993 32.000484999999998,-105.078604999999996 32.000532999999997,-105.077045999999996 32.000578999999995,-104.918272000000002 32.000495999999991,-104.643525999999994 32.000442999999997,-104.640917999999999 32.000396000000002,-104.531936999999999 32.000311000000004,-104.531756 32.000117000000003,-104.024520999999993 32.000010000000003,-103.980179000000007 32.000124999999997,-103.875476000000006 32.000554,-103.748317 32.000197999999997,-103.72373965094468 32.000208021677786,-103.713395184678689 32.000212239744897,-103.713395184670219 32.000212239744904,-103.713395184654971 32.000212239744911,-103.713395184639609 32.000212239744918,-103.60161412595366 32.000257819670985,-103.326500999999993 32.00036999999999,-103.278520999999998 32.000419,-103.270382999999995 32.000326,-103.267707999999999 32.000323999999992,-103.267633000000004 32.000475000000002,-103.215641000000005 32.000512999999998,-103.133028835827503 32.000473953106116,-103.088697999999994 32.000452999999993,-103.085875999999985 32.000464999999998,-103.064422999999991 32.000518,-103.064344000000006 32.087051000000002,-103.064347999999995 32.123041,-103.064421999999993 32.145006000000002,-103.064695999999998 32.522193,-103.064761000000004 32.587983,-103.064787999999993 32.600397,-103.064761000000004 32.601863000000002,-103.064814999999996 32.624536999999997,-103.064633 32.646419999999992,-103.064864 32.682647000000003,-103.064797999999996 32.690761000000002,-103.064798999999994 32.708694,-103.064826999999994 32.726627999999998,-103.064807000000002 32.777302999999996,-103.064698000000007 32.783602000000002,-103.064711000000003 32.784593,-103.064699000000005 32.827531,-103.064672000000002 32.828470000000003,-103.064888999999994 32.849359,-103.064915999999982 32.857259999999997,-103.064807000000002 32.857695999999997,-103.064862000000005 32.868346000000003,-103.064700999999985 32.879354999999997,-103.064569000000006 32.900013999999999,-103.064656999999997 32.959097,-103.064678999999998 32.964373000000002,-103.064625000000007 32.999898999999999,-103.064452000000003 33.010289999999998,-103.06398 33.038693000000002,-103.063904999999991 33.042054999999998,-103.063382198608849 33.066417104805645,-103.0628188509327 33.092668632365545,-103.062136190569035 33.12448003074244,-103.060102999999984 33.219225000000002,-103.059719999999999 33.256262,-103.059241999999998 33.260370999999992,-103.057856 33.315233999999997,-103.057486999999995 33.32947699999999,-103.056655000000006 33.388437999999994,-103.052609999999987 33.570599,-103.051664000000002 33.629489,-103.051362999999995 33.64195,-103.051535 33.650486999999998,-103.051086999999981 33.658186,-103.050532000000004 33.672407999999997,-103.050147999999993 33.701971,-103.049608000000006 33.737766,-103.049096000000006 33.746270000000003,-103.047346000000005 33.824674999999999,-103.046907000000004 33.850299999999997,-103.045643999999996 33.901536999999998,-103.045698000000002 33.90629899999999,-103.044893000000002 33.945616999999999,-103.043949999999981 33.974629,-103.043616999999998 34.003633,-103.043531000000002 34.018014,-103.043554999999984 34.032713999999991,-103.043745999999999 34.037294000000003,-103.043771000000007 34.041538000000003,-103.043720999999991 34.042319999999997,-103.043767000000003 34.043544999999995,-103.043744000000004 34.049985999999997,-103.04368599999998 34.063077999999997,-103.043515999999983 34.079382000000003,-103.043569000000005 34.087947,-103.043571175003294 34.092846731402311,-103.043572866326429 34.096656853966635,-103.04358003846437 34.112813863799865,-103.043644 34.256903,-103.043718999999982 34.289440999999997,-103.043935999999988 34.302584999999993,-103.043978999999979 34.312763999999994,-103.043947553639484 34.376410480771497,-103.043947499824213 34.37651940122479,-103.043946000000005 34.379555000000003,-103.043943999999982 34.37966,-103.043919000000002 34.380915999999992,-103.043693000000005 34.383578,-103.043629999999993 34.384689999999999,-103.043614000000005 34.384968999999998,-103.043612999999993 34.388679000000003,-103.043612999999993 34.390442,-103.043606047260084 34.391254973944719,-103.043584999999993 34.393715999999998,-103.043610999999999 34.397105000000003,-103.043582999999998 34.400677999999999,-103.043537999999998 34.405462999999997,-103.043582 34.455657000000002,-103.043587999999986 34.459662000000002,-103.043588999999997 34.459774000000003,-103.043593999999985 34.46266,-103.043434927764125 34.510540742996007,-103.043071999999981 34.619782,-103.043277851519235 34.65183038816317,-103.0432796163159 34.65210514391012,-103.043285999999995 34.653098999999997,-103.042827000000003 34.671187999999994,-103.042768999999993 34.747360999999998,-103.042770000000004 34.792223999999997,-103.042781000000005 34.850242999999999,-103.042520999999979 34.899546,-103.0425974544018 35.032467348285799,-103.042641999999987 35.109912999999999,-103.043261 35.125058000000003,-103.042519999999996 35.135596,-103.042599999999993 35.142766000000002,-103.042710999999997 35.144734999999997,-103.042568000000003 35.159317999999999,-103.042394999999999 35.178573,-103.042338999999984 35.181922,-103.042366 35.182786,-103.042377000000002 35.183149,-103.042496999999997 35.211862000000004,-103.042775000000006 35.241236999999998,-103.042366 35.250056,-103.041554000000005 35.622487,-103.041484634909168 35.651235429903174,-103.041145999999998 35.791583000000003,-103.041916999999998 35.796441000000002,-103.041715999999994 35.814072000000003,-103.042186 35.825216999999995,-103.04130499999998 35.837693999999999,-103.040823999999986 36.055230999999992,-103.041387523024824 36.229129564686382,-103.041674 36.317534000000002,-103.041745000000006 36.318266999999999,-103.041923999999995 36.500439,-103.00243399999998 36.500397)),((-96.818513748151517 28.172441936006489,-96.816443421354066 28.174808025412563,-96.791958210781445 28.188687337716829,-96.733036581469989 28.190913261798631,-96.70383764358462 28.198245729538105,-96.697421731775194 28.2029594609741,-96.702659211994316 28.211208487181011,-96.703313894801497 28.216445964862743,-96.686815839850283 28.218410020896432,-96.662461568376486 28.227313732447904,-96.66062845144161 28.228884976259902,-96.663116248646261 28.233205895474203,-96.651855673914184 28.251275190431315,-96.607991796426646 28.277069769155677,-96.608122730450688 28.280081317680857,-96.611527096272326 28.281390688369981,-96.610479598706064 28.283092866206033,-96.592934048725994 28.296972182316367,-96.581018783574592 28.302209665072866,-96.553260151988255 28.302340591484768,-96.546975176740276 28.305614018207578,-96.542130508742858 28.315958034472228,-96.528905880514458 28.322504882843088,-96.476269225261575 28.330753906512612,-96.45099839278295 28.337038879223222,-96.434107525610045 28.344764159184386,-96.418918843885365 28.35484630740093,-96.415252604940861 28.362833452872817,-96.417216660974546 28.367154372087118,-96.412895739222876 28.36951123780511,-96.403206408302779 28.371475291301415,-96.401242352269094 28.366892498964241,-96.400558825492723 28.362187299887704,-96.398667286465624 28.349166496619617,-96.397846 28.343512999999998,-96.413700000000006 28.327342999999999,-96.439098999999999 28.319051999999999,-96.495600517783487 28.293964130304811,-96.547774000000004 28.270797999999999,-96.587113635820756 28.248878526668094,-96.611036668198281 28.235548960715661,-96.621533999999983 28.229699999999998,-96.694665999999998 28.18212,-96.758140999999995 28.136872999999998,-96.830820125734775 28.079724674840076,-96.849624000000006 28.064938999999999,-96.854049189790473 28.060788472935577,-96.855594496383262 28.059339080448503,-96.859762278796424 28.055429983997605,-96.896530613737951 28.020943786452513,-96.898080497387397 28.019490100997579,-96.929052999999996 27.990439999999996,-96.96699599999998 27.950531000000002,-97.001440999999986 27.911442,-97.031660000000002 27.869974999999997,-97.041798999999997 27.852925999999997,-97.045408776308051 27.837452569961062,-97.045409609838757 27.837448997003069,-97.04598 27.835004,-97.050599498412936 27.830109781194331,-97.058265753683145 27.821987615677948,-97.076304165860961 27.802876463727326,-97.079565315161531 27.799421374709375,-97.083535350812468 27.795215242050556,-97.085211615730273 27.793439290085495,-97.085394999999991 27.793244999999999,-97.11422230692618 27.755303327915222,-97.116276999999997 27.752599,-97.117304906633777 27.751048809529347,-97.118830196202964 27.748748513687826,-97.139140047561696 27.718119138409879,-97.139351757288111 27.717799858049549,-97.143807202733072 27.711080581371732,-97.165547551982726 27.678293865995041,-97.166583141799606 27.676732088482499,-97.166681999999994 27.676583,-97.184850261879149 27.644709535797791,-97.192144670654784 27.631912600211269,-97.198729900630283 27.620359811436909,-97.201865999999995 27.614857999999998,-97.211651066704889 27.596044174137351,-97.213608899397855 27.592279833590187,-97.221943360361436 27.576255098965802,-97.265194083077162 27.493096589152017,-97.265746568141566 27.492034321708481,-97.276090999999994 27.472145,-97.304469999999995 27.407734,-97.326522999999995 27.347611999999998,-97.347445832595042 27.277936119324231,-97.350397999999998 27.268104999999998,-97.363400999999996 27.210366,-97.370941000000002 27.161165999999998,-97.377001000000007 27.101020999999999,-97.379130000000004 27.047996,-97.378361999999996 26.992877,-97.370730999999992 26.909706,-97.365301177044699 26.875333999999995,-97.365282052674758 26.875212938438843,-97.364726000000005 26.871692999999997,-97.363633430415348 26.866515420001111,-97.352028350385282 26.811520085064032,-97.35141299999998 26.808603999999999,-97.350529648372714 26.805138580575584,-97.333027999999999 26.736478999999996,-97.304204860681779 26.646364129642233,-97.300690000000003 26.635375,-97.297812715808362 26.627503004299889,-97.297022484076606 26.625340999999999,-97.287871455917369 26.600304594305239,-97.275119000000004 26.565415000000002,-97.269391999999982 26.554046,-97.269132970349304 26.553256905349773,-97.257954534248029 26.519203490608863,-97.229844 26.433568999999999,-97.223724090442104 26.411478908273114,-97.206682644704898 26.34996703527348,-97.194643999999997 26.306512999999999,-97.185844000000003 26.267102999999995,-97.177979856301576 26.220346386353309,-97.177168840972797 26.215524458900923,-97.173264999999986 26.192314,-97.170387765328073 26.167037808112223,-97.169872392484734 26.162510314053797,-97.167099005951158 26.138146416702778,-97.161470999999992 26.088705,-97.158662630175101 26.080176913346069,-97.154270970414501 26.066840900880429,-97.161462285840642 26.067639941946975,-97.169842256221145 26.077853024187579,-97.171781452365479 26.102521767945923,-97.179531587141199 26.146202099560895,-97.178745972847352 26.177103221942961,-97.183983445454302 26.214289306886101,-97.194458400817808 26.271639686993655,-97.214884565299002 26.353606215503973,-97.226930759399721 26.385554824668418,-97.240286324189555 26.405980989149626,-97.240845398636537 26.411470089808617,-97.243166933615896 26.434263367616055,-97.247618791929028 26.456260775909261,-97.254165635225121 26.471187589585856,-97.262545605605609 26.482971915638458,-97.27642492679071 26.521729238684483,-97.292399243108349 26.528014213932472,-97.310730412457033 26.556558470596535,-97.308111681228326 26.5712234060755,-97.308635417324538 26.576722756880109,-97.317015387705027 26.597672667607057,-97.318434072834165 26.60022630157264,-97.324871601690262 26.611813856840271,-97.338489044677715 26.647428701777493,-97.33639404954522 26.666021744249065,-97.345821512417203 26.700589103038251,-97.363105189274393 26.710540307081217,-97.370437657013895 26.723895871871047,-97.370961403259614 26.736203954318924,-97.367557047587525 26.740393934434408,-97.364152671616353 26.758986971196869,-97.364646278833362 26.767121661774897,-97.368866408127118 26.774699404876429,-97.370437646864346 26.781508126370166,-97.368342661881371 26.795649318140761,-97.373056398392137 26.808481136684847,-97.387459455673451 26.820789208983193,-97.383531343606066 26.875520854055946,-97.385626348888096 26.88887641440536,-97.391649435788921 26.901970109878395,-97.389554460955466 26.945964918852653,-97.390340075249313 27.05228571820243,-97.389816329003565 27.067212531879022,-97.386411973331462 27.083186832972341,-97.387459455673422 27.090519300711819,-97.390601943297412 27.094185534581555,-97.390078197051665 27.156511519247964,-97.377508246555678 27.199458835730731,-97.379865109736301 27.202863196477598,-97.386673841379576 27.204696313412462,-97.382221972916923 27.229050589961044,-97.37488951532697 27.25026236588155,-97.373318276589728 27.276449754290233,-97.372896734486858 27.277942715507475,-97.367033301341749 27.298709035706306,-97.36467643816114 27.302637142698917,-97.359962701650375 27.304732132756659,-97.357605838469766 27.307874620380655,-97.362319574980518 27.32672953597509,-97.366771423144115 27.333276389420721,-97.36179581858525 27.359987519000377,-97.346607126710992 27.390364889744752,-97.336132171347501 27.402411088920228,-97.331156576938184 27.412362295500575,-97.329585328051422 27.418123524502818,-97.330894688591016 27.425455992242295,-97.329847196099536 27.434359703793774,-97.317277255753083 27.463689574751687,-97.293708603647858 27.497209429631152,-97.282971770086746 27.521563701104967,-97.267627232015485 27.541048841138782,-97.26651977444007 27.542455137362143,-97.266473727822444 27.542513609294531,-97.257831889393842 27.556392930479635,-97.261144559602045 27.562355746269706,-97.261759991311692 27.563463525096239,-97.260450620622564 27.56739163716362,-97.260303240478223 27.567589679048492,-97.252732825146552 27.577762415194865,-97.252070650242061 27.578652211895708,-97.247885553160842 27.581360217573209,-97.247618802078478 27.581532821322067,-97.236881968517352 27.598292751933528,-97.24107194863285 27.602482732049012,-97.241084821027414 27.602523494839662,-97.242643187370078 27.607458346757408,-97.231683715414945 27.631671123728168,-97.231553918442799 27.631957884363242,-97.231382612637987 27.632336350521356,-97.221955159915538 27.632860099304484,-97.221711030183002 27.632638163711842,-97.219074540339648 27.630241360463614,-97.217418907944051 27.630677052710489,-97.214098935780783 27.631550728615359,-97.200743370990949 27.650143776161688,-97.197339005169326 27.664546838517776,-97.19760088336696 27.678426154628117,-97.203474366386629 27.684532651921231,-97.20308902068453 27.688000774441392,-97.200450811776179 27.688974477466285,-97.190006537429824 27.692829222058965,-97.188284576858237 27.695272543577584,-97.170627865440125 27.720325976082009,-97.166176017276527 27.732372180332245,-97.161200407642937 27.734074342943998,-97.153606061705801 27.733288721037997,-97.147321086457822 27.735383711095739,-97.130823034043985 27.751619892924076,-97.127942424617643 27.75580987303956,-97.127680546420009 27.759999858229808,-97.125046985317312 27.763143140340585,-97.103326269871417 27.789067861139614,-97.102999946318647 27.791923200131116,-97.10255066693685 27.795854405604921,-97.102495980878558 27.796332909939515,-97.102278777379922 27.798233445813956,-97.100865886772965 27.802472121847156,-97.098874421707819 27.808446522979796,-97.095168997779496 27.812151946908138,-97.092851314507953 27.81446963017968,-97.092589446459854 27.819183356540908,-97.098874421707819 27.822849590410645,-97.104263831428213 27.822227735056668,-97.126109297533233 27.819707102786651,-97.130299287798252 27.820492727230029,-97.134489267913736 27.825206463740791,-97.132394272781227 27.827301438574235,-97.10670775348396 27.832389859144307,-97.087681973298601 27.836158807756469,-97.056712719518927 27.842293721800239,-97.055822986229373 27.843403727743151,-97.04322620548075 27.859119113080268,-97.013634476624247 27.906780143237341,-97.017955395838555 27.911493874673329,-97.016384146951765 27.917255079570445,-96.985744902767337 27.954048356415132,-96.977888688782102 27.976438572489602,-96.978805242174786 27.978271691961854,-96.980900237307296 27.978271691961854,-96.986006780964971 27.976176699366729,-96.986661461234775 27.980759491703903,-96.978281501003806 28.001709402430844,-96.967806540565562 28.020040576854303,-96.966759042999328 28.020367911280101,-96.965187799187319 28.013297317932185,-96.962569062883844 28.012380759464751,-96.952617853766114 28.01643980428749,-96.946987563862692 28.02652194996665,-96.932453562407773 28.035425661518129,-96.926430465357427 28.043412814602167,-96.929572952981431 28.051399967686208,-96.927085150701998 28.057292130712504,-96.906004298338829 28.076147047258459,-96.890946545563423 28.076801730065643,-96.886232814127425 28.084396073465374,-96.88832780925992 28.086622002621937,-96.889113433703315 28.099453823703399,-96.886887499471996 28.117130312782294,-96.879424092633712 28.131402425890027,-96.874972236857971 28.133235547899659,-96.87078225420511 28.13127149186597,-96.86462821551855 28.126295887307109,-96.857164811217643 28.115559058820768,-96.845380482627661 28.108881273888468,-96.830128556765757 28.111822717849165,-96.827049318353744 28.112416571196771,-96.816574357915513 28.119618104912195,-96.810420321766344 28.126034014184238,-96.810944073086844 28.136378030448888,-96.816836228500989 28.158048094801106,-96.820248352517055 28.163388807027481,-96.822859330626116 28.167475552598326,-96.818513748151517 28.172441936006489)))" - } - }, - { - "pk": 2, - "model": "geoapp.country", - "fields": { - "name": "New Zealand", - "mpoly": "SRID=4326;MULTIPOLYGON (((174.616364000000004 -36.100861000000002,174.634978999999987 -36.124718,174.708862000000011 -36.205559,174.781096999999988 -36.266945,174.812744000000009 -36.339165,174.768311000000011 -36.34639,174.710784999999987 -36.525832999999999,174.708587999999992 -36.533332999999999,174.70773299999999 -36.541671999999998,174.714690999999988 -36.595832999999999,174.717194000000006 -36.603057999999997,174.774414000000007 -36.730277999999998,174.808319000000012 -36.805275000000002,174.854400999999996 -36.847777999999998,174.896636999999998 -36.878334000000002,175.011658000000011 -36.871941,175.020813000000004 -36.873610999999997,175.055542000000003 -36.880279999999999,175.076629999999994 -36.890839,175.082458000000003 -36.895279000000002,175.087463000000014 -36.900832999999999,175.091644000000002 -36.925559999999997,175.161652000000004 -36.955559,175.221069 -36.937775000000002,175.230255 -36.939438000000003,175.278320000000008 -36.965279000000002,175.310516000000007 -36.995002999999997,175.319976999999994 -37.005836000000002,175.323853000000014 -37.012222,175.328856999999999 -37.026947,175.330261000000007 -37.035277999999998,175.330535999999995 -37.044449,175.321899000000002 -37.06472,175.31942699999999 -37.095275999999998,175.317748999999992 -37.144165,175.319121999999993 -37.152495999999999,175.328856999999999 -37.168892,175.373290999999995 -37.216659999999997,175.385254000000003 -37.225830000000002,175.404967999999997 -37.228050000000003,175.579131999999987 -37.244446000000003,175.58914200000001 -37.169449,175.551636000000002 -37.024718999999997,175.547484999999995 -37.009171000000002,175.542480000000012 -36.994446000000003,175.536102 -36.980826999999998,175.524993999999992 -36.961669999999998,175.498566000000011 -36.927222999999998,175.484406000000007 -36.920279999999998,175.476348999999999 -36.917777999999998,175.464416999999997 -36.90889,175.435241999999988 -36.866942999999999,175.46691899999999 -36.809998,175.509430000000009 -36.776108,175.486358999999993 -36.679726000000002,175.463593000000003 -36.621383999999999,175.378296000000006 -36.570557,175.366364000000004 -36.561667999999997,175.361633000000012 -36.556389000000003,175.356628 -36.541671999999998,175.353850999999992 -36.525002,175.35244800000001 -36.489998,175.353577 -36.481940999999999,175.360229000000004 -36.478050000000003,175.538025000000005 -36.514724999999999,175.542755 -36.519996999999996,175.604950000000002 -36.622771999999998,175.631072999999986 -36.710555999999997,175.763610999999997 -36.713614999999997,175.840789999999998 -36.754173000000002,175.733582000000013 -36.805832000000002,175.701629999999994 -36.844161999999997,175.707458000000003 -36.869995000000003,175.709960999999993 -36.875275000000002,175.714690999999988 -36.880828999999999,175.720519999999993 -36.885277000000002,175.735779000000008 -36.891387999999999,175.74383499999999 -36.893889999999999,175.758330999999998 -36.876106,175.761382999999995 -36.869446000000003,175.757751000000013 -36.863059999999997,175.753051999999997 -36.857779999999998,175.749114999999989 -36.851394999999997,175.75 -36.843055999999997,175.752196999999995 -36.835555999999997,175.756378000000012 -36.830002,175.765533000000005 -36.828055999999997,175.808013999999986 -36.824173000000002,175.817200000000014 -36.825836000000002,175.833587999999992 -36.830832999999998,175.845519999999993 -36.839722000000002,175.849120999999997 -36.846107000000003,175.878570999999994 -36.914444000000003,175.881072999999986 -36.921669,175.918304000000006 -37.067504999999997,175.917480000000012 -37.075836000000002,175.915253000000007 -37.083061,175.898314999999997 -37.115004999999996,175.884154999999993 -37.168892,175.883330999999998 -37.176949,175.887206999999989 -37.244163999999998,175.892212 -37.249724999999998,175.921082000000013 -37.251671000000002,175.927185000000009 -37.256110999999997,175.93081699999999 -37.262504999999997,175.936371000000008 -37.278885000000002,175.940093999999988 -37.299717,175.974396000000013 -37.418059999999997,175.976074000000011 -37.453055999999997,176.026931999999988 -37.483108999999999,176.035583000000003 -37.487282,176.059417999999994 -37.502502,176.088287000000008 -37.525557999999997,176.093291999999991 -37.531112999999998,176.165526999999997 -37.613892,176.169433999999995 -37.620277000000002,176.167205999999993 -37.625832000000003,176.160247999999996 -37.624167999999997,176.082458000000003 -37.602500999999997,176.066924999999998 -37.59639,176.061095999999992 -37.591942000000003,176.057189999999991 -37.585555999999997,176.062744000000009 -37.580832999999998,176.070800999999989 -37.577781999999999,176.088287000000008 -37.580002,176.095245000000006 -37.576110999999997,176.090239999999994 -37.570838999999999,176.062468999999993 -37.546669,176.023087000000004 -37.528221000000002,176.018767999999994 -37.526057999999999,176.013931000000014 -37.524554999999999,176.007598999999999 -37.524222999999999,175.955535999999995 -37.521110999999998,175.946349999999995 -37.523055999999997,175.940796000000006 -37.527779000000002,175.953583000000009 -37.558891000000003,175.994415000000004 -37.638893000000003,176.071899000000002 -37.655273,176.14498900000001 -37.675277999999999,176.242187999999999 -37.709442000000003,176.267486999999988 -37.676392,176.488281 -37.756667999999998,176.521636999999998 -37.769447,176.527771 -37.773887999999999,176.537475999999998 -37.784447,176.549712999999997 -37.793334999999999,176.656646999999992 -37.855559999999997,176.671082000000013 -37.862502999999997,176.686645999999996 -37.868332000000002,176.759154999999993 -37.892775999999998,176.784149000000014 -37.900275999999998,176.80304000000001 -37.903328000000002,176.819702000000007 -37.904442000000003,176.838287000000008 -37.907501000000003,176.917755 -37.926108999999997,176.943848000000003 -37.932502999999997,177.08273299999999 -37.967216,177.107468000000011 -37.987220999999998,177.159424 -38.013336000000002,177.415253000000007 -37.982498,177.47357199999999 -37.962502,177.545806999999996 -37.919167000000002,177.552459999999996 -37.915000999999997,177.571625000000012 -37.902222000000002,177.599120999999997 -37.878051999999997,177.603577 -37.872498,177.646941999999996 -37.805,177.732178000000005 -37.682502999999997,177.738861000000014 -37.678336999999999,177.746917999999994 -37.675277999999999,177.791931000000005 -37.666946000000003,177.849396000000013 -37.657218999999998,177.858856000000003 -37.656661999999997,177.868010999999996 -37.654442000000003,177.875792999999987 -37.651389999999999,178.0 -37.592224000000002,178.006653 -37.588332999999999,178.012206999999989 -37.583328000000002,178.018004999999988 -37.550831000000002,178.05581699999999 -37.542777999999998,178.064972000000012 -37.542228999999999,178.187744000000009 -37.546951,178.281921000000011 -37.560828999999998,178.306915000000004 -37.568061999999998,178.311919999999986 -37.573334000000003,178.312468999999993 -37.578887999999999,178.306915000000004 -37.583610999999998,178.321075000000008 -37.602500999999997,178.336365 -37.618332000000002,178.34970100000001 -37.626944999999999,178.367737000000005 -37.630828999999999,178.448028999999991 -37.645279000000002,178.457458000000003 -37.646667,178.468018 -37.646949999999997,178.488861000000014 -37.644165,178.497192000000013 -37.646667,178.504424999999998 -37.649994,178.550536999999991 -37.6875,178.555542000000003 -37.692497000000003,178.559692000000013 -37.698883000000002,178.56552099999999 -37.713332999999999,178.562468999999993 -37.719994,178.483306999999996 -37.826393000000003,178.455230999999998 -37.860000999999997,178.449982000000006 -37.865004999999996,178.429687999999999 -37.876944999999999,178.419983000000002 -37.887779000000002,178.350525000000005 -38.004722999999998,178.347473000000008 -38.011116,178.347197999999992 -38.019722000000002,178.348846000000009 -38.027779000000002,178.354950000000002 -38.032218999999998,178.360229000000004 -38.037506,178.364136000000002 -38.043616999999998,178.375792999999987 -38.072777000000002,178.378296000000006 -38.089995999999999,178.377746999999999 -38.09861,178.353850999999992 -38.185555,178.319976999999994 -38.248055,178.318024000000008 -38.255561999999998,178.317474000000004 -38.263893000000003,178.320800999999989 -38.398612999999997,178.302459999999996 -38.528885000000002,178.300536999999991 -38.536391999999999,178.296356000000003 -38.541946000000003,178.158874999999995 -38.649169999999998,178.074982000000006 -38.713889999999999,178.068297999999999 -38.717773,178.060241999999988 -38.721107000000003,178.050812000000008 -38.721381999999998,178.044433999999995 -38.717216,177.928864000000004 -38.722220999999998,177.941070999999994 -38.793616999999998,177.923858999999993 -38.918334999999999,177.917786000000007 -38.942802,177.909697999999992 -38.969718999999998,177.897705000000002 -39.047942999999997,177.893859999999989 -39.064776999999999,177.906708000000009 -39.064278000000002,177.923034999999999 -39.089165,177.942200000000014 -39.091942000000003,177.967743000000013 -39.098334999999999,177.991332999999997 -39.115004999999996,177.996612999999996 -39.120277000000002,177.999390000000005 -39.127495000000003,177.909973000000008 -39.256950000000003,177.898865 -39.267502,177.874968999999993 -39.286118000000002,177.868010999999996 -39.290283000000002,177.861908 -39.285834999999999,177.844970999999987 -39.251396,177.839416999999997 -39.236946000000003,177.824127000000004 -39.193053999999997,177.823577999999998 -39.183883999999999,177.826629999999994 -39.177222999999998,177.841644000000002 -39.152779000000002,177.822021000000007 -39.114445000000003,177.823195999999996 -39.110115,177.82351700000001 -39.105110000000003,177.82119800000001 -39.101275999999999,177.816696000000007 -39.099274,177.680266999999986 -39.075279000000002,177.628845000000013 -39.071114,177.426085999999998 -39.064163,177.387755999999996 -39.077781999999999,177.246917999999994 -39.128334000000002,177.206085000000002 -39.143616000000002,177.149138999999991 -39.165000999999997,177.054961999999989 -39.204445,176.935790999999995 -39.349997999999999,176.931365999999997 -39.355559999999997,176.903870000000012 -39.398055999999997,176.898865 -39.412216,176.89776599999999 -39.438048999999999,176.899414000000007 -39.446387999999999,176.946625000000012 -39.664444000000003,177.01080300000001 -39.654998999999997,177.107178000000005 -39.660828000000002,177.116913000000011 -39.662216,177.120789000000002 -39.66861,177.119110000000006 -39.676108999999997,177.115783999999991 -39.682502999999997,177.08273299999999 -39.729996,177.073577999999998 -39.741385999999999,177.068024000000008 -39.746108999999997,177.059692000000013 -39.749167999999997,177.050262000000004 -39.751396,177.033874999999995 -39.757506999999997,177.028045999999989 -39.762504999999997,177.023590000000013 -39.768059,177.020263999999997 -39.774718999999997,176.894713999999993 -40.034728999999999,176.890533000000005 -40.049728000000002,176.889983999999998 -40.058052000000004,176.89498900000001 -40.082779000000002,176.893035999999995 -40.090279000000002,176.874114999999989 -40.121383999999999,176.834136999999998 -40.181671,176.808319000000012 -40.216659999999997,176.796935999999988 -40.226387000000003,176.687195000000003 -40.321387999999999,176.644135000000006 -40.379997000000003,176.628296000000006 -40.421944000000003,176.539977999999991 -40.495002999999997,176.521362000000011 -40.513893000000003,176.500823999999994 -40.535004,176.441924999999998 -40.600281000000003,176.405243000000013 -40.643889999999999,176.386108000000007 -40.675002999999997,176.35522499999999 -40.688606,176.349396000000013 -40.693610999999997,176.288574000000011 -40.793892,176.239136000000002 -40.90889,176.22079500000001 -40.931671,176.195526 -40.941665999999998,176.172760000000011 -40.952224999999999,176.158600000000007 -40.959999000000003,176.152771 -40.964722000000002,176.142212 -40.975273,176.134154999999993 -40.987502999999997,176.120789000000002 -41.013618,176.11300700000001 -41.035277999999998,176.098297000000002 -41.087218999999997,176.087463000000014 -41.116112,176.080811000000011 -41.129165999999998,176.062195000000003 -41.151938999999999,175.984679999999997 -41.231383999999998,175.955230999999998 -41.255279999999999,175.819121999999993 -41.347220999999998,175.743561 -41.392226999999998,175.736358999999993 -41.396110999999998,175.557738999999998 -41.485000999999997,175.471069 -41.541389000000002,175.427459999999996 -41.564444999999999,175.323028999999991 -41.614449,175.313292999999987 -41.616394,175.230804000000006 -41.620834000000002,175.222197999999992 -41.618332000000002,175.21691899999999 -41.612777999999999,175.184692000000013 -41.535834999999999,175.181915000000004 -41.519447,175.181091000000009 -41.500838999999999,175.188568000000004 -41.461112999999997,175.193024000000008 -41.446106,175.193848000000003 -41.437775000000002,175.191070999999994 -41.430557,175.186095999999992 -41.425002999999997,175.080261000000007 -41.385559,175.063018999999997 -41.380279999999999,175.05304000000001 -41.378608999999997,175.045532000000009 -41.378608999999997,175.027190999999988 -41.381667999999998,174.993286000000012 -41.393332999999998,174.985779000000008 -41.397224,174.972747999999996 -41.405830000000002,174.960509999999999 -41.415275999999999,174.949982000000006 -41.425559999999997,174.944976999999994 -41.431114,174.94165000000001 -41.437775000000002,174.936919999999986 -41.443328999999999,174.918578999999994 -41.448051,174.908600000000007 -41.448334000000003,174.872467 -41.429442999999999,174.86605800000001 -41.425002999999997,174.861084000000005 -41.419724000000002,174.861633000000012 -41.347496,174.863861000000014 -41.340279000000002,174.870789000000002 -41.327224999999999,174.87912 -41.315002,174.883881000000002 -41.309441,174.894440000000003 -41.290000999999997,174.89776599999999 -41.282501000000003,174.899993999999992 -41.275002,174.901916999999997 -41.258338999999999,174.901916999999997 -41.251114,174.898865 -41.234444000000003,174.89498900000001 -41.228050000000003,174.887482000000006 -41.224442000000003,174.827179 -41.218887000000002,174.818848000000003 -41.221938999999999,174.787475999999998 -41.244446000000003,174.778045999999989 -41.255836000000002,174.774414000000007 -41.262222,174.771087999999992 -41.278053,174.778594999999996 -41.281387000000002,174.796082000000013 -41.286949,174.818024000000008 -41.284728999999999,174.824127000000004 -41.289169,174.829407000000003 -41.294724000000002,174.833313000000004 -41.301108999999997,174.832184000000012 -41.309441,174.82995600000001 -41.316665999999998,174.826355000000007 -41.323334000000003,174.821625000000012 -41.328887999999999,174.815796000000006 -41.333610999999998,174.807189999999991 -41.336387999999999,174.744689999999991 -41.347496,174.700531000000012 -41.344444000000003,174.671906000000007 -41.338332999999999,174.654694000000006 -41.333061,174.648314999999997 -41.328612999999997,174.629669000000007 -41.315002,174.59191899999999 -41.27861,174.591644000000002 -41.271385000000002,174.594116000000014 -41.263893000000003,174.601074000000011 -41.250838999999999,174.608306999999996 -41.237777999999999,174.61300700000001 -41.232216,174.619110000000006 -41.23111,174.627746999999999 -41.233887000000003,174.637482000000006 -41.235557999999997,174.648590000000013 -41.234444000000003,174.666931000000005 -41.229720999999998,174.681365999999997 -41.221938999999999,174.694702000000007 -41.213614999999997,174.712738000000002 -41.199440000000003,174.718567000000007 -41.194716999999997,174.800812000000008 -41.100281000000003,174.844421000000011 -41.041671999999998,174.874390000000005 -41.018059,174.882721000000004 -41.015006999999997,174.892486999999988 -41.013061999999998,174.901093000000003 -41.010283999999999,174.908324999999991 -41.006393000000003,174.932189999999991 -40.987502999999997,174.94165000000001 -40.976387000000003,174.945251000000013 -40.969718999999998,174.947478999999987 -40.962218999999997,175.014983999999998 -40.84861,175.09857199999999 -40.755836000000002,175.112731999999994 -40.738892,175.120789000000002 -40.726944000000003,175.127746999999999 -40.713614999999997,175.164153999999996 -40.631943,175.169708000000014 -40.616942999999999,175.171906000000007 -40.609726000000002,175.187468999999993 -40.530830000000002,175.23800700000001 -40.329726999999998,175.23135400000001 -40.280830000000002,175.201629999999994 -40.181671,175.196349999999995 -40.166946000000003,175.178314 -40.134171000000002,175.15554800000001 -40.095832999999999,175.071625000000012 -40.003059,175.055542000000003 -39.987777999999999,175.022217000000012 -39.958053999999997,174.986358999999993 -39.93,174.974120999999997 -39.920836999999999,174.960784999999987 -39.912773,174.938873 -39.902222000000002,174.923034999999999 -39.895836000000003,174.836638999999991 -39.865004999999996,174.828307999999993 -39.862502999999997,174.790802000000014 -39.854720999999998,174.781096999999988 -39.853057999999997,174.751373 -39.865555,174.740509000000003 -39.866661,174.729674999999986 -39.865836999999999,174.57607999999999 -39.829169999999998,174.558013999999986 -39.824722,174.549408 -39.822226999999998,174.542205999999993 -39.818610999999997,174.523865 -39.805,174.421356000000003 -39.726944000000003,174.410521999999986 -39.716942000000003,174.376068000000004 -39.678612,174.353577 -39.639999000000003,174.348846000000009 -39.634444999999999,174.336638999999991 -39.625557,174.316070999999994 -39.613892,174.30886799999999 -39.610283000000003,174.217194000000006 -39.579726999999998,174.208862000000011 -39.577224999999999,174.040802000000014 -39.552779999999998,173.997741999999988 -39.551392,173.986908 -39.550552000000003,173.970245000000006 -39.545279999999998,173.963012999999989 -39.541671999999998,173.871062999999992 -39.483330000000002,173.851898000000006 -39.470551,173.839966000000004 -39.461387999999999,173.810790999999995 -39.4375,173.79525799999999 -39.421944000000003,173.786652000000004 -39.409996,173.775817999999987 -39.390839,173.769713999999993 -39.376944999999999,173.762206999999989 -39.354720999999998,173.754424999999998 -39.305,173.751923000000005 -39.288612,173.751647999999989 -39.269996999999996,173.781921000000011 -39.191383000000002,173.785247999999996 -39.184998,173.800536999999991 -39.169167000000002,173.82995600000001 -39.145836000000003,173.844421000000011 -39.138336000000002,173.868010999999996 -39.128883000000002,173.892761000000007 -39.120552000000004,174.011108000000007 -39.073334000000003,174.114685000000009 -39.024445,174.187744000000009 -38.988608999999997,174.209136999999998 -38.977218999999998,174.226623999999987 -38.972496,174.248016000000007 -38.970551,174.259978999999987 -38.970275999999998,174.281372000000005 -38.969994,174.293029999999987 -38.969994,174.313292999999987 -38.972496,174.351348999999999 -38.979438999999999,174.375244000000009 -38.979163999999997,174.384704999999997 -38.977218999999998,174.392761000000007 -38.974442000000003,174.456359999999989 -38.940277000000002,174.546082000000013 -38.871941,174.557738999999998 -38.860557999999997,174.568024000000008 -38.850281000000003,174.587738000000002 -38.828887999999999,174.594421000000011 -38.815834000000002,174.603302000000014 -38.786118000000002,174.608582000000013 -38.763061999999998,174.625518999999997 -38.677779999999998,174.642486999999988 -38.590836000000003,174.681091000000009 -38.379165999999998,174.724396000000013 -38.185828999999998,174.838561999999996 -38.157218999999998,174.848021999999986 -38.156944000000003,174.85635400000001 -38.154167,174.926636000000002 -38.116112,174.932465000000008 -38.111389000000003,174.940246999999999 -38.101112,174.898590000000013 -38.075004999999997,174.877166999999986 -38.064163,174.892486999999988 -37.976387000000003,174.893585000000002 -37.969994,174.868286000000012 -37.943610999999997,174.861084000000005 -37.939995000000003,174.85522499999999 -37.944716999999997,174.834686000000005 -37.963614999999997,174.830261000000007 -37.969161999999997,174.828033000000005 -37.976661999999997,174.828583000000009 -37.995002999999997,174.822754000000003 -37.999724999999998,174.814696999999995 -38.002502,174.804137999999995 -38.001944999999999,174.797211000000004 -37.998336999999999,174.791077 -37.993614,174.786376999999987 -37.988334999999999,174.783874999999995 -37.980826999999998,174.783600000000007 -37.971663999999997,174.788025000000005 -37.868332000000002,174.788879000000009 -37.860000999999997,174.791077 -37.852783000000002,174.795532000000009 -37.846947,174.819702000000007 -37.829169999999998,174.826629999999994 -37.825279000000002,174.841339000000005 -37.818610999999997,174.872741999999988 -37.806106999999997,174.883330999999998 -37.805,174.903045999999989 -37.807502999999997,174.945251000000013 -37.810555,174.967467999999997 -37.809165999999998,174.975525000000005 -37.806389000000003,174.974975999999998 -37.75,174.974670000000003 -37.740836999999999,174.966644000000002 -37.739998,174.951355000000007 -37.743057,174.94442699999999 -37.745002999999997,174.93081699999999 -37.752502,174.906921000000011 -37.774169999999998,174.870789000000002 -37.783057999999997,174.861633000000012 -37.785004,174.848297000000002 -37.769722000000002,174.828307999999993 -37.710830999999999,174.764160000000004 -37.527779000000002,174.744415000000004 -37.487502999999997,174.724975999999998 -37.447220000000002,174.717467999999997 -37.425002999999997,174.714690999999988 -37.408607000000003,174.714690999999988 -37.399445,174.719116000000014 -37.393616000000002,174.725799999999992 -37.389999000000003,174.744415000000004 -37.385834000000003,174.760528999999991 -37.380279999999999,174.767486999999988 -37.376389000000003,174.773041000000006 -37.371665999999998,174.831635000000006 -37.308052000000004,174.840789999999998 -37.296951,174.840515000000011 -37.291389000000002,174.82995600000001 -37.290557999999997,174.821899000000002 -37.291671999999998,174.812744000000009 -37.293616999999998,174.804687999999999 -37.296393999999999,174.797760000000011 -37.300277999999999,174.766083000000009 -37.321114,174.754700000000014 -37.330559,174.751373 -37.336945,174.744965000000008 -37.359169,174.740509000000003 -37.364722999999998,174.733582000000013 -37.368606999999997,174.724120999999997 -37.368889000000003,174.717194000000006 -37.365279999999998,174.711365 -37.360557999999997,174.701629999999994 -37.349724000000002,174.69442699999999 -37.336945,174.660187000000008 -37.273311999999997,174.645813000000004 -37.236389000000003,174.639709000000011 -37.224442000000003,174.599975999999998 -37.153885000000002,174.578307999999993 -37.115555,174.569976999999994 -37.103614999999998,174.555542000000003 -37.087502,174.551909999999992 -37.080832999999998,174.549712999999997 -37.073616,174.55304000000001 -37.067222999999998,174.558593999999999 -37.0625,174.568848000000003 -37.061385999999999,174.644135000000006 -37.061110999999997,174.661376999999987 -37.065551999999997,174.664977999999991 -37.071944999999999,174.666381999999999 -37.080283999999999,174.661925999999994 -37.085830999999999,174.650542999999999 -37.095275999999998,174.646087999999992 -37.100838000000003,174.648590000000013 -37.108055,174.703856999999999 -37.197777000000002,174.733856000000003 -37.196106,174.716644000000002 -37.154167,174.72357199999999 -37.150275999999998,174.87912 -37.088889999999999,174.887482000000006 -37.059165999999998,174.795532000000009 -37.023055999999997,174.804413000000011 -36.972220999999998,174.824982000000006 -36.960830999999999,174.829407000000003 -36.955002,174.83273299999999 -36.948608,174.828033000000005 -36.943053999999997,174.818848000000003 -36.941383000000002,174.770263999999997 -36.936661,174.696625000000012 -36.93972,174.686095999999992 -36.940834000000002,174.659697999999992 -36.947495000000004,174.653045999999989 -36.951393000000003,174.641662999999994 -36.960830999999999,174.622467 -36.982216,174.619110000000006 -36.988892,174.617187999999999 -36.996108999999997,174.600525000000005 -37.022224,174.524688999999995 -37.045555,174.515533000000005 -37.045555,174.508605999999986 -37.041946000000003,174.502472000000012 -37.037506,174.498016000000007 -37.031944000000003,174.490783999999991 -37.019165,174.484679999999997 -37.005561999999998,174.459228999999993 -36.944031000000003,174.451904000000013 -36.923889000000003,174.446930000000009 -36.909163999999997,174.443024000000008 -36.893616000000002,174.439147999999989 -36.868606999999997,174.436645999999996 -36.852226000000002,174.43386799999999 -36.835555999999997,174.427764999999994 -36.812775000000002,174.422760000000011 -36.798057999999997,174.416655999999989 -36.784171999999998,174.409697999999992 -36.771385000000002,174.406096999999988 -36.765006999999997,174.390533000000005 -36.740279999999998,174.344299000000007 -36.679336999999997,174.301085999999998 -36.62722,174.284973000000008 -36.612777999999999,174.267486999999988 -36.599167,174.236908 -36.568061999999998,174.208862000000011 -36.535277999999998,174.187744000000009 -36.496948000000003,174.17804000000001 -36.467498999999997,174.177764999999994 -36.458336000000003,174.178864000000004 -36.449997000000003,174.181091000000009 -36.442497000000003,174.184417999999994 -36.436110999999997,174.189972000000012 -36.431389000000003,174.196930000000009 -36.427779999999998,174.20495600000001 -36.428612,174.24383499999999 -36.440834000000002,174.251923000000005 -36.443610999999997,174.257751000000013 -36.448051,174.300262000000004 -36.515839,174.348846000000009 -36.601944000000003,174.367919999999998 -36.629165999999998,174.370728000000014 -36.6325,174.422484999999995 -36.667220999999998,174.430542000000003 -36.668059999999997,174.453033000000005 -36.651108,174.456359999999989 -36.644722000000002,174.465515000000011 -36.582779000000002,174.465515000000011 -36.532218999999998,174.442474000000004 -36.414718999999998,174.422211000000004 -36.366112,174.41885400000001 -36.370834000000002,174.391937000000013 -36.395004,174.384978999999987 -36.398887999999999,174.377166999999986 -36.401665,174.303864000000004 -36.395279000000002,174.296935999999988 -36.391669999999998,174.285247999999996 -36.3825,174.276916999999997 -36.370552000000004,174.26998900000001 -36.357779999999998,174.268585000000002 -36.349442000000003,174.268585000000002 -36.342224000000002,174.292205999999993 -36.316947999999996,174.299988000000013 -36.314163,174.308013999999986 -36.316947999999996,174.325531000000012 -36.330283999999999,174.334686000000005 -36.332222000000002,174.36883499999999 -36.331673000000002,174.376891999999998 -36.330832999999998,174.421248999999989 -36.310611999999999,174.50692699999999 -36.267220000000002,174.518311000000011 -36.258057,174.521636999999998 -36.251396,174.519135000000006 -36.245834000000002,174.505248999999992 -36.231383999999998,174.413299999999992 -36.263061999999998,174.378296000000006 -36.286667,174.364685000000009 -36.294167000000002,174.346619000000004 -36.298057999999997,174.336365 -36.298889000000003,174.305237000000005 -36.287506,174.365233999999987 -36.260002,174.442200000000014 -36.169724000000002,174.396087999999992 -36.144447,174.396362000000011 -36.151938999999999,174.394135000000006 -36.159163999999997,174.373290999999995 -36.206389999999999,174.369965000000008 -36.213057999999997,174.358856000000003 -36.222220999999998,174.345245000000006 -36.229720999999998,174.337463000000014 -36.232773000000002,174.33273299999999 -36.229163999999997,174.307738999999998 -36.176949,174.282745000000006 -36.121108999999997,174.274993999999992 -36.118332000000002,174.239409999999992 -36.111389000000003,174.229126000000008 -36.112502999999997,174.195251000000013 -36.131110999999997,174.188568000000004 -36.144165,174.185790999999995 -36.171944000000003,174.191345000000013 -36.176392,174.19830300000001 -36.18,174.217743000000013 -36.182502999999997,174.226898000000006 -36.182502999999997,174.236084000000005 -36.180557,174.244110000000006 -36.183059999999998,174.287749999999988 -36.210281000000002,174.312468999999993 -36.236663999999998,174.314696999999995 -36.243889000000003,174.267212 -36.270279000000002,174.259430000000009 -36.273055999999997,174.251373 -36.272499000000003,174.065796000000006 -36.168334999999999,173.999114999999989 -36.121108999999997,173.994415000000004 -36.115836999999999,173.99105800000001 -36.109444000000003,173.931091000000009 -35.981940999999999,173.938568000000004 -35.934998,173.947478999999987 -35.923889000000003,173.948577999999998 -35.915550000000003,173.946075000000008 -35.908051,173.913879000000009 -35.86972,173.908324999999991 -35.874442999999999,173.903870000000012 -35.879997000000003,173.904967999999997 -35.888336000000002,173.921356000000003 -36.003059,173.980804000000006 -36.121383999999999,173.987731999999994 -36.134171000000002,173.992461999999989 -36.139724999999999,174.124390000000005 -36.263618,174.176085999999998 -36.276947,174.180542000000003 -36.282501000000003,174.198853000000014 -36.343887000000002,174.199982000000006 -36.352226000000002,174.198028999999991 -36.368606999999997,174.194702000000007 -36.375275000000002,174.189147999999989 -36.379722999999998,174.115233999999987 -36.40361,174.089966000000004 -36.411385000000003,174.080811000000011 -36.409438999999999,174.06942699999999 -36.400275999999998,174.065796000000006 -36.393889999999999,174.057465000000008 -36.374718,174.049133000000012 -36.353614999999998,174.044433999999995 -36.338889999999999,174.039703000000003 -36.323334000000003,174.031096999999988 -36.293059999999997,174.020537999999988 -36.273887999999999,173.82607999999999 -36.032501000000003,173.737457000000006 -35.933608999999997,173.727172999999993 -35.923614999999998,173.590515000000011 -35.778053,173.398865 -35.573891000000003,173.395537999999988 -35.567504999999997,173.398865 -35.553612,173.446625000000012 -35.440277000000002,173.465789999999998 -35.429169000000002,173.501923000000005 -35.419724000000002,173.506378000000012 -35.425277999999999,173.540253000000007 -35.430832000000002,173.629943999999995 -35.356667000000002,173.65554800000001 -35.322502,173.65554800000001 -35.313332000000003,173.564147999999989 -35.278053,173.556091000000009 -35.280830000000002,173.551636000000002 -35.286391999999999,173.549712999999997 -35.302779999999998,173.549712999999997 -35.321114,173.551909999999992 -35.328612999999997,173.566924999999998 -35.344161999999997,173.568024000000008 -35.361671,173.566924999999998 -35.36972,173.559143000000006 -35.372771999999998,173.439696999999995 -35.376106,173.416077 -35.384444999999999,173.410247999999996 -35.388893000000003,173.391356999999999 -35.423057999999997,173.39498900000001 -35.449722,173.396087999999992 -35.458053999999997,173.392761000000007 -35.482773000000002,173.386382999999995 -35.523330999999999,173.380524000000008 -35.528053,173.37384 -35.524169999999998,173.306915000000004 -35.449165,173.237731999999994 -35.370277000000002,173.154144000000002 -35.276665,173.103302000000014 -35.226104999999997,173.09191899999999 -35.216942000000003,173.087463000000014 -35.211387999999999,173.09191899999999 -35.1875,173.098846000000009 -35.183608999999997,173.119110000000006 -35.185555,173.12802099999999 -35.1875,173.134704999999997 -35.191108999999997,173.14498900000001 -35.191940000000002,173.152771 -35.189163,173.163879000000009 -35.179169000000002,173.168578999999994 -35.173614999999998,173.18081699999999 -35.156104999999997,173.187468999999993 -35.143332999999998,173.191924999999998 -35.128334000000002,173.196349999999995 -35.104446000000003,173.197478999999987 -35.09639,173.198577999999998 -35.078887999999999,173.198577999999998 -35.051108999999997,173.193848000000003 -35.027222000000002,173.189423000000005 -35.012779000000002,173.179413000000011 -34.993332000000002,173.175812000000008 -34.986946000000003,173.159149000000014 -34.958336000000003,173.151093000000003 -34.946663,173.137755999999996 -34.93,172.947204999999997 -34.716942000000003,172.828033000000005 -34.584442000000003,172.812468999999993 -34.568893000000003,172.722473000000008 -34.495277000000002,172.739136000000002 -34.435555,172.900817999999987 -34.414718999999998,172.911925999999994 -34.414718999999998,173.020813000000004 -34.422226000000002,173.038879000000009 -34.436942999999999,173.043304000000006 -34.517775999999998,173.039977999999991 -34.522224,173.033324999999991 -34.525832999999999,173.024414000000007 -34.527779000000002,173.016662999999994 -34.52861,173.007476999999994 -34.526947,172.997467 -34.498336999999999,172.984130999999991 -34.472771000000002,172.961914000000007 -34.465279000000002,172.955230999999998 -34.468887000000002,172.908324999999991 -34.544167000000002,172.912749999999988 -34.549728000000002,172.923858999999993 -34.558891000000003,172.936095999999992 -34.567222999999998,172.973021999999986 -34.581116000000002,173.053314 -34.665550000000003,173.057738999999998 -34.680283000000003,173.110503999999992 -34.791389000000002,173.120789000000002 -34.810555,173.128570999999994 -34.822502,173.133026 -34.828055999999997,173.13857999999999 -34.832504,173.151916999999997 -34.839995999999999,173.213593000000003 -34.871941,173.242737000000005 -34.884726999999998,173.273314999999997 -34.940834000000002,173.268859999999989 -34.946387999999999,173.262206999999989 -34.959167,173.259978999999987 -34.966659999999997,173.258881000000002 -34.974716,173.259978999999987 -34.983055,173.264709000000011 -35.014449999999997,173.269135000000006 -35.019722000000002,173.317474000000004 -35.018889999999999,173.328583000000009 -35.009444999999999,173.358856000000003 -34.98111,173.36300700000001 -34.975555,173.366364000000004 -34.968887000000002,173.373259999999988 -34.936774999999997,173.371246000000014 -34.927444,173.400817999999987 -34.863334999999999,173.426085999999998 -34.82,173.450806 -34.807777000000002,173.500274999999988 -34.864722999999998,173.499114999999989 -34.871108999999997,173.492461999999989 -34.874718,173.452453999999989 -34.887779000000002,173.443297999999999 -34.889724999999999,173.433319000000012 -34.890555999999997,173.413025000000005 -34.888893000000003,173.405243000000013 -34.892502,173.399719000000005 -34.897224,173.399719000000005 -34.906387000000002,173.402190999999988 -34.913887000000003,173.411590999999987 -34.931778,173.419128 -34.945830999999998,173.426909999999992 -34.957779000000002,173.431641000000013 -34.963332999999999,173.44165000000001 -34.973328000000002,173.454131999999987 -34.981667000000002,173.467467999999997 -34.988892,173.475525000000005 -34.991669000000002,173.493286000000012 -34.995277000000002,173.541655999999989 -34.988608999999997,173.561645999999996 -34.958893000000003,173.562468999999993 -34.950836000000002,173.566924999999998 -34.936110999999997,173.572478999999987 -34.931389000000003,173.579131999999987 -34.927779999999998,173.589416999999997 -34.928612,173.83914200000001 -35.004173000000002,174.101074000000011 -35.121108999999997,174.098297000000002 -35.161667,174.091644000000002 -35.158051,174.056641000000013 -35.155555999999997,174.021911999999986 -35.164161999999997,174.008026 -35.207504,174.008026 -35.215004,174.011382999999995 -35.221381999999998,174.018311000000011 -35.224997999999999,174.093841999999995 -35.225273,174.100525000000005 -35.228881999999999,174.143585000000002 -35.328612999999997,174.208008000000007 -35.323334000000003,174.218018 -35.322226999999998,174.247741999999988 -35.27861,174.319976999999994 -35.232773000000002,174.383881000000002 -35.337775999999998,174.461638999999991 -35.445273999999998,174.491913000000011 -35.485275,174.575806 -35.601944000000003,174.577179 -35.610000999999997,174.577147999999994 -35.618088,174.569976999999994 -35.648055999999997,174.563292999999987 -35.651665,174.530272999999994 -35.649445,174.520263999999997 -35.648612999999997,174.513306 -35.645004,174.505248999999992 -35.642502,174.479126000000008 -35.641945,174.47357199999999 -35.646393000000003,174.474975999999998 -35.654716,174.518311000000011 -35.724167,174.523041000000006 -35.729438999999999,174.554137999999995 -35.750281999999999,174.561919999999986 -35.753059,174.583862000000011 -35.764724999999999,174.60244800000001 -35.844444000000003,174.596924 -35.849167,174.58914200000001 -35.851944000000003,174.577758999999986 -35.852226000000002,174.560790999999995 -35.847777999999998,174.554961999999989 -35.843330000000002,174.538574000000011 -35.819450000000003,174.523314999999997 -35.796669,174.519713999999993 -35.790283000000002,174.49105800000001 -35.769447,174.361908 -35.723328000000002,174.357451999999995 -35.72361,174.34857199999999 -35.734726000000002,174.346924 -35.833061,174.350525000000005 -35.839438999999999,174.35522499999999 -35.845001000000003,174.360779000000008 -35.849442000000003,174.381348000000003 -35.851112,174.384704999999997 -35.844718999999998,174.385528999999991 -35.828887999999999,174.391083000000009 -35.824173000000002,174.398865 -35.821387999999999,174.437468999999993 -35.822777000000002,174.476074000000011 -35.824173000000002,174.486358999999993 -35.824722,174.519713999999993 -35.844718999999998,174.525542999999999 -35.849167,174.524414000000007 -35.855559999999997,174.521362000000011 -35.862220999999998,174.501373 -35.889999000000003,174.496917999999994 -35.895553999999997,174.487182999999987 -35.915000999999997,174.495513999999986 -35.988608999999997,174.498016000000007 -35.994163999999998,174.514160000000004 -36.008614,174.56942699999999 -36.037224000000002,174.578583000000009 -36.038894999999997,174.587738000000002 -36.038894999999997,174.596619000000004 -36.036667,174.606902999999988 -36.037506,174.612731999999994 -36.042228999999999,174.622192000000013 -36.053055,174.62912 -36.065834000000002,174.629394999999988 -36.075004999999997,174.616364000000004 -36.100861000000002)),((171.185241999999988 -44.938332000000003,171.182129000000003 -44.94173,171.175017999999994 -44.949902000000002,171.167923000000002 -44.959850000000003,171.165436 -44.966599000000002,171.164703000000003 -44.970275999999998,171.163299999999992 -44.978332999999999,171.149719000000005 -44.996665999999998,171.07995600000001 -45.067222999999998,171.02664200000001 -45.103332999999999,170.975525000000005 -45.151108,170.966063999999989 -45.163055,170.961914000000007 -45.169167000000002,170.921630999999991 -45.243057,170.873566000000011 -45.358055,170.873290999999995 -45.367218,170.876891999999998 -45.373885999999999,170.871338000000009 -45.424171,170.859955000000014 -45.487502999999997,170.855804000000006 -45.493889000000003,170.750274999999988 -45.61972,170.674682999999987 -45.745002999999997,170.616913000000011 -45.839438999999999,170.55886799999999 -45.881667999999998,170.554413000000011 -45.888053999999997,170.555542000000003 -45.896110999999998,170.591063999999989 -45.894165,170.600525000000005 -45.891669999999998,170.608582000000013 -45.888053999999997,170.658600000000007 -45.858612,170.718018 -45.817504999999997,170.724670000000003 -45.813057,170.773314999999997 -45.782775999999998,170.781096999999988 -45.786391999999999,170.790802000000014 -45.806946000000003,170.790802000000014 -45.84639,170.788879000000009 -45.863892,170.783051 -45.878334000000002,170.776366999999993 -45.882773999999998,170.699982000000006 -45.911667,170.664429000000013 -45.908332999999999,170.57330300000001 -45.916663999999997,170.550536999999991 -45.919167000000002,170.484406000000007 -45.926949,170.452453999999989 -45.931946000000003,170.424408 -45.939438000000003,170.382721000000004 -45.956108,170.342467999999997 -45.97361,170.315246999999999 -45.991385999999999,170.309692000000013 -45.996948000000003,170.294433999999995 -46.013893000000003,170.281646999999992 -46.029442000000003,170.263610999999997 -46.051940999999999,170.256378000000012 -46.065551999999997,170.253051999999997 -46.081947,170.252472000000012 -46.09111,170.254424999999998 -46.107779999999998,170.252777000000009 -46.115836999999999,170.240783999999991 -46.146949999999997,170.237731999999994 -46.154167,170.226623999999987 -46.165000999999997,170.214416999999997 -46.174720999999998,170.193848000000003 -46.188048999999999,170.169433999999995 -46.198608,170.067748999999992 -46.246665999999998,169.911925999999994 -46.340279000000002,169.863585999999998 -46.371383999999999,169.858001999999999 -46.376663,169.849396000000013 -46.389442000000003,169.848021999999986 -46.416946000000003,169.849975999999998 -46.433326999999998,169.852172999999993 -46.440834000000002,169.853850999999992 -46.457504,169.850799999999992 -46.464722000000002,169.845245000000006 -46.469994,169.701904000000013 -46.558052000000004,169.631348000000003 -46.581947,169.458008000000007 -46.623328999999998,169.266388000000006 -46.656387000000002,169.133026 -46.670836999999999,169.107727000000011 -46.669167000000002,169.097197999999992 -46.666946000000003,169.084411999999986 -46.657501000000003,169.067200000000014 -46.635002,169.061919999999986 -46.675002999999997,169.053864000000004 -46.678612,169.008026 -46.680832000000002,168.883330999999998 -46.667777999999998,168.878296000000006 -46.661942000000003,168.861908 -46.628334000000002,168.862457000000006 -46.619446000000003,168.864409999999992 -46.611114999999998,168.864959999999996 -46.603889000000002,168.844970999999987 -46.563614,168.835784999999987 -46.560555,168.823853000000014 -46.561110999999997,168.733306999999996 -46.577499000000003,168.640533000000005 -46.603889000000002,168.633605999999986 -46.606392,168.614685000000009 -46.611114999999998,168.593018 -46.614165999999997,168.568848000000003 -46.615004999999996,168.516937000000013 -46.614165999999997,168.492737000000005 -46.613334999999999,168.442138999999997 -46.601500999999999,168.436630000000008 -46.599666999999997,168.446625000000012 -46.584003000000003,168.442200000000014 -46.575004999999997,168.454407000000003 -46.574447999999997,168.490509000000003 -46.573059,168.502472000000012 -46.574173000000002,168.506103999999993 -46.578887999999999,168.499114999999989 -46.583328000000002,168.505553999999989 -46.588051,168.551636000000002 -46.594444000000003,168.561371000000008 -46.591942000000003,168.566924999999998 -46.586661999999997,168.563599000000011 -46.580002,168.550812000000008 -46.570557,168.541655999999989 -46.567504999999997,168.391356999999999 -46.540000999999997,168.361908 -46.544449,168.353577 -46.547783000000003,168.351623999999987 -46.556106999999997,168.352172999999993 -46.564444999999999,168.362731999999994 -46.583885000000002,168.348236000000014 -46.582165000000003,168.352904999999993 -46.584499,168.359267999999986 -46.585830999999999,168.365082 -46.592666999999999,168.367248999999987 -46.596668,168.367599000000013 -46.601497999999999,168.364928999999989 -46.605331,168.35775799999999 -46.604500000000002,168.351409999999987 -46.603164999999997,168.275269000000009 -46.557777000000002,168.269135000000006 -46.535561,168.268311000000011 -46.529167,168.27664200000001 -46.525832999999999,168.286102 -46.523330999999999,168.307738999999998 -46.520553999999997,168.318297999999999 -46.520836000000003,168.318848000000003 -46.529167,168.321075000000008 -46.536667,168.328856999999999 -46.540557999999997,168.339416999999997 -46.539169,168.386658000000011 -46.497779999999999,168.392486999999988 -46.492226000000002,168.395537999999988 -46.485000999999997,168.396087999999992 -46.477775999999999,168.394135000000006 -46.470275999999998,168.37384 -46.421944000000003,168.36883499999999 -46.416106999999997,168.359679999999997 -46.415000999999997,168.350249999999988 -46.417503000000004,168.250274999999988 -46.400832999999999,168.211090000000013 -46.355277999999998,168.206085000000002 -46.351394999999997,168.194702000000007 -46.345551,168.185516000000007 -46.342498999999997,168.174988000000013 -46.340279000000002,168.116364000000004 -46.343612999999998,168.063873 -46.351669,167.955535999999995 -46.371383999999999,167.893035999999995 -46.387222,167.850799999999992 -46.399445,167.832184000000012 -46.398612999999997,167.824401999999992 -46.394447,167.753875999999991 -46.333885000000002,167.776916999999997 -46.312775000000002,167.781372000000005 -46.306389000000003,167.783600000000007 -46.298340000000003,167.781372000000005 -46.290840000000003,167.778045999999989 -46.284171999999998,167.701904000000013 -46.209442000000003,167.596924 -46.166389000000002,167.556641000000013 -46.156387000000002,167.546356000000003 -46.154167,167.534424 -46.152779000000002,167.483032000000009 -46.149726999999999,167.471069 -46.149994,167.460236000000009 -46.151389999999999,167.451904000000013 -46.154716,167.446075000000008 -46.159996,167.415526999999997 -46.204720000000002,167.356628 -46.254447999999996,167.280272999999994 -46.273055999999997,167.261108000000007 -46.267775999999998,167.238555999999988 -46.263893000000003,167.086638999999991 -46.240555,167.001373 -46.229163999999997,166.966063999999989 -46.224716,166.949982000000006 -46.223885000000003,166.915253000000007 -46.226104999999997,166.883026 -46.229996,166.836365 -46.232498,166.823028999999991 -46.231667000000002,166.768035999999995 -46.22361,166.726348999999999 -46.214165,166.717467999999997 -46.210830999999999,166.705230999999998 -46.201110999999997,166.670806999999996 -46.161667,166.67025799999999 -46.15361,166.684417999999994 -46.145004,166.693848000000003 -46.142775999999998,166.709411999999986 -46.135277000000002,166.738861000000014 -46.119163999999998,166.761230000000012 -46.093612999999998,166.784911999999991 -46.064945000000002,166.804748999999987 -46.033279,166.82995600000001 -46.003891000000003,166.854674999999986 -45.994163999999998,166.886932000000002 -45.990279999999998,166.943297999999999 -45.956108,166.949127000000004 -45.950836000000002,166.946930000000009 -45.945273999999998,166.939147999999989 -45.944716999999997,166.929687999999999 -45.947220000000002,166.836365 -45.981383999999998,166.828033000000005 -45.984444000000003,166.791350999999992 -46.005004999999997,166.785797000000002 -46.010283999999999,166.781096999999988 -46.016396,166.759308000000004 -46.035553,166.762969999999996 -46.038387,166.764969000000008 -46.042392999999997,166.763656999999995 -46.047221999999998,166.760162000000008 -46.050392000000002,166.740982000000002 -46.066059000000003,166.737793000000011 -46.066558999999998,166.670532000000009 -46.087218999999997,166.615783999999991 -46.090836000000003,166.613585999999998 -46.086387999999999,166.617461999999989 -46.059722999999998,166.621062999999992 -46.052498,166.640807999999993 -46.015006999999997,166.661652000000004 -45.991942999999999,166.625792999999987 -45.967216,166.492461999999989 -46.013061999999998,166.484406000000007 -46.014449999999997,166.474975999999998 -46.002785000000003,166.467194000000006 -45.990555,166.465239999999994 -45.983055,166.464690999999988 -45.839438999999999,166.468841999999995 -45.823059,166.472473000000008 -45.815834000000002,166.476898000000006 -45.809722999999998,166.537475999999998 -45.798889000000003,166.613861000000014 -45.801108999999997,166.652190999999988 -45.800277999999999,166.699982000000006 -45.798889000000003,166.881348000000003 -45.779998999999997,166.890807999999993 -45.777779000000002,166.974120999999997 -45.735000999999997,166.987182999999987 -45.709724,166.923034999999999 -45.705832999999998,166.912475999999998 -45.705275999999998,166.887482000000006 -45.705002,166.854126000000008 -45.707779000000002,166.825806 -45.714722000000002,166.809417999999994 -45.721381999999998,166.787475999999998 -45.689995000000003,166.775817999999987 -45.662773,166.81153900000001 -45.617942999999997,166.811217999999997 -45.613109999999999,166.812531000000007 -45.608111999999998,166.816710999999998 -45.605606000000002,166.869689999999991 -45.588779000000002,166.881026999999989 -45.586112999999997,166.88736 -45.585278000000002,166.895203000000009 -45.585608999999998,166.901366999999993 -45.587108999999998,166.912871999999993 -45.590946000000002,166.980255 -45.578612999999997,166.991913000000011 -45.580002,167.0 -45.576949999999997,167.005829000000006 -45.571671000000002,167.032470999999987 -45.527779000000002,167.041350999999992 -45.501396,166.989685000000009 -45.519165,166.891693000000004 -45.544724000000002,166.796875 -45.569389,166.791199000000006 -45.570720999999999,166.784836000000013 -45.571556,166.777023000000014 -45.571219999999997,166.710509999999999 -45.579445,166.704407000000003 -45.574447999999997,166.697204999999997 -45.554718,166.699127000000004 -45.546669,166.757476999999994 -45.425277999999999,166.801361000000014 -45.353614999999998,166.821625000000012 -45.320557,166.86883499999999 -45.279724000000002,166.880797999999999 -45.279167,167.008330999999998 -45.341667,167.038299999999992 -45.357779999999998,167.146941999999996 -45.427222999999998,167.16885400000001 -45.472496,167.205230999999998 -45.477775999999999,167.211914000000007 -45.475273,167.21414200000001 -45.467216,167.209411999999986 -45.461387999999999,167.173584000000005 -45.422775,167.158019999999993 -45.406661999999997,167.134978999999987 -45.386116,167.12912 -45.381385999999999,167.115509000000003 -45.372222999999998,167.097747999999996 -45.366112,167.087463000000014 -45.363616999999998,167.061645999999996 -45.345001000000003,167.051085999999998 -45.333328000000002,167.081421000000006 -45.325333,167.08407600000001 -45.321666999999998,167.092407000000009 -45.316498000000003,167.097243999999989 -45.314498999999998,167.114227 -45.310333,167.163574000000011 -45.302833999999997,167.169922000000014 -45.302998000000002,167.173584000000005 -45.305999999999997,167.176422000000002 -45.309330000000003,167.17756700000001 -45.313834999999997,167.212738000000002 -45.313332000000003,167.218841999999995 -45.318336000000002,167.232178000000005 -45.327224999999999,167.239685000000009 -45.331116000000002,167.248566000000011 -45.334167,167.260254000000003 -45.335830999999999,167.270813000000004 -45.334442000000003,167.301636000000002 -45.329445,167.309692000000013 -45.326110999999997,167.307738999999998 -45.318610999999997,167.300262000000004 -45.314444999999999,167.19464099999999 -45.271445999999997,167.138290000000012 -45.268943999999998,167.099471999999992 -45.272114000000002,167.094146999999992 -45.271278000000002,167.08964499999999 -45.268776000000003,167.002196999999995 -45.201110999999997,166.996917999999994 -45.145836000000003,167.146362000000011 -45.001671000000002,167.198577999999998 -44.956108,167.204407000000003 -44.951110999999997,167.228302000000014 -44.930832000000002,167.24105800000001 -44.921387000000003,167.260528999999991 -44.907501000000003,167.267486999999988 -44.903053,167.312744000000009 -44.875,167.320800999999989 -44.871383999999999,167.393462999999997 -44.862999000000002,167.398987000000005 -44.861496000000002,167.404312000000004 -44.863498999999997,167.421798999999993 -44.894168999999998,167.422974000000011 -44.898665999999999,167.420638999999994 -44.908496999999997,167.417968999999999 -44.912166999999997,167.440796000000006 -44.928612,167.439972000000012 -44.9375,167.441924999999998 -44.945,167.459686000000005 -44.99472,167.497741999999988 -45.003891000000003,167.507202000000007 -45.001396,167.508224000000013 -45.0,167.511658000000011 -44.995277000000002,167.511108000000007 -44.986946000000003,167.47908000000001 -44.903446000000002,167.465912000000003 -44.876441999999997,167.462752999999992 -44.867947,167.461578000000003 -44.863444999999999,167.460097999999988 -44.853946999999998,167.459411999999986 -44.843944999999998,167.459914999999995 -44.838444000000003,167.448577999999998 -44.795279999999998,167.453033000000005 -44.788894999999997,167.458587999999992 -44.783614999999998,167.59970100000001 -44.683883999999999,167.743010999999996 -44.611671,167.838866999999993 -44.498610999999997,167.850249999999988 -44.488052000000003,167.949982000000006 -44.40361,167.963593000000003 -44.395004,168.034973000000008 -44.353614999999998,168.12802099999999 -44.316947999999996,168.141662999999994 -44.308052000000004,168.144713999999993 -44.300834999999999,168.144135000000006 -44.292503000000004,168.124099999999999 -44.251404,168.143311000000011 -44.253059,168.153594999999996 -44.251396,168.169708000000014 -44.24472,168.288299999999992 -44.172775,168.29385400000001 -44.167503000000004,168.336638999999991 -44.123885999999999,168.339966000000004 -44.116661,168.337738000000002 -44.109169,168.334411999999986 -44.102783000000002,168.336365 -44.094444000000003,168.369689999999991 -44.043334999999999,168.374968999999993 -44.037781000000003,168.383026 -44.034447,168.402771 -44.029724000000002,168.672211000000004 -43.987777999999999,168.676909999999992 -43.993614,168.683043999999995 -43.998336999999999,168.690246999999999 -44.002228000000002,168.715239999999994 -44.012222,168.723021999999986 -44.012504999999997,168.753875999999991 -44.010002,168.764160000000004 -44.008614,168.824401999999992 -43.988334999999999,168.858582000000013 -43.976661999999997,168.866364000000004 -43.973328000000002,168.879669000000007 -43.964447,168.885254000000003 -43.959167,168.961914000000007 -43.90361,169.080765000000014 -43.848472999999998,169.142212 -43.794167000000002,169.224396000000013 -43.743332000000002,169.271636999999998 -43.722496,169.387482000000006 -43.679169000000002,169.491318000000007 -43.643467,169.539153999999996 -43.633614,169.62661700000001 -43.613892,169.64498900000001 -43.608893999999999,169.652771 -43.605559999999997,169.660521999999986 -43.601944000000003,169.721619000000004 -43.573334000000003,169.728302000000014 -43.568893000000003,169.739136000000002 -43.558052000000004,169.767486999999988 -43.522773999999998,169.790526999999997 -43.496665999999998,169.871062999999992 -43.406661999999997,169.884154999999993 -43.397781000000002,169.961638999999991 -43.371941,170.021911999999986 -43.352500999999997,170.028594999999996 -43.347777999999998,170.033874999999995 -43.342498999999997,170.038025000000005 -43.336112999999997,170.049408 -43.306946000000003,170.111358999999993 -43.254173000000002,170.288299999999992 -43.107779999999998,170.418029999999987 -43.052498,170.523041000000006 -43.010559,170.531921000000011 -43.008057,170.583037999999988 -42.990555,170.674347000000012 -42.958739999999999,170.704407000000003 -42.945830999999998,170.750549000000007 -42.924720999999998,170.781096999999988 -42.910553,170.794128 -42.901389999999999,171.065796000000006 -42.648055999999997,171.108306999999996 -42.608055,171.149719000000005 -42.563614,171.153594999999996 -42.55722,171.196075000000008 -42.476661999999997,171.225799999999992 -42.433608999999997,171.230255 -42.40889,171.235779000000008 -42.394165,171.247467 -42.375,171.260254000000003 -42.366112,171.270537999999988 -42.355003000000004,171.297760000000011 -42.310279999999999,171.304413000000011 -42.296669,171.309692000000013 -42.281944000000003,171.31665000000001 -42.249724999999998,171.321075000000008 -42.224716,171.322754000000003 -42.207222000000002,171.324401999999992 -42.18972,171.326355000000007 -42.172226000000002,171.329131999999987 -42.155830000000002,171.343841999999995 -42.110832000000002,171.361084000000005 -42.067779999999999,171.461638999999991 -41.859726000000002,171.51080300000001 -41.764449999999997,171.533051 -41.766396,171.557738999999998 -41.766663,171.569121999999993 -41.766112999999997,171.650817999999987 -41.761391000000003,171.663025000000005 -41.757781999999999,171.685790999999995 -41.746948000000003,171.790526999999997 -41.696387999999999,171.85522499999999 -41.652779000000002,171.886658000000011 -41.629997000000003,171.942200000000014 -41.550277999999999,172.02276599999999 -41.443053999999997,172.053864000000004 -41.417503000000004,172.064972000000012 -41.40361,172.122192000000013 -41.277779000000002,172.111633000000012 -41.236114999999998,172.10522499999999 -41.153053,172.106078999999994 -40.911385000000003,172.10635400000001 -40.893059,172.108856000000003 -40.885559,172.113861000000014 -40.879997000000003,172.186645999999996 -40.813332000000003,172.218567000000007 -40.785834999999999,172.22470100000001 -40.781109,172.255248999999992 -40.776947,172.263884999999988 -40.774169999999998,172.271362000000011 -40.770553999999997,172.299408 -40.755004999999997,172.348297000000002 -40.727493000000003,172.382721000000004 -40.698334000000003,172.426909999999992 -40.657775999999998,172.478302000000014 -40.613892,172.513320999999991 -40.598441999999999,172.517639000000003 -40.596783000000002,172.523482999999999 -40.597946,172.522644000000014 -40.602943000000003,172.521132999999992 -40.607277000000003,172.520813000000004 -40.625,172.519440000000003 -40.631385999999999,172.525542999999999 -40.6325,172.534149000000014 -40.631667999999998,172.571899000000002 -40.617775000000002,172.596343999999988 -40.601394999999997,172.62802099999999 -40.573891000000003,172.631621999999993 -40.567222999999998,172.631621999999993 -40.559998,172.608582000000013 -40.556946000000003,172.601348999999999 -40.558891000000003,172.597014999999999 -40.553333000000002,172.589690999999988 -40.558838000000002,172.583862000000011 -40.560004999999997,172.581024000000014 -40.556671,172.581848000000008 -40.551665999999997,172.589843999999999 -40.541172000000003,172.627166999999986 -40.511947999999997,172.633330999999998 -40.509171000000002,172.661376999999987 -40.502785000000003,172.712188999999995 -40.495552000000004,172.817748999999992 -40.504173000000002,172.861358999999993 -40.507781999999999,172.981628 -40.527222000000002,172.991332999999997 -40.529167,172.989959999999996 -40.53389,172.978026999999997 -40.53389,172.945251000000013 -40.530830000000002,172.895263999999997 -40.524445,172.797211000000004 -40.516112999999997,172.739959999999996 -40.516945,172.731628 -40.519722000000002,172.656921000000011 -40.653328000000002,172.701355000000007 -40.748336999999999,172.855804000000006 -40.853057999999997,172.865509000000003 -40.853057999999997,172.875244000000009 -40.851112,172.907195999999999 -40.828887999999999,172.930266999999986 -40.802222999999998,172.935241999999988 -40.796669,172.976623999999987 -40.781944000000003,172.985229000000004 -40.781112999999998,173.002196999999995 -40.786667,173.008330999999998 -40.791114999999998,173.013306 -40.796669,173.020537999999988 -40.809722999999998,173.051085999999998 -40.869446000000003,173.059692000000013 -40.962775999999998,173.059692000000013 -40.971938999999999,173.056091000000009 -40.978606999999997,173.031646999999992 -41.027495999999999,173.075806 -41.291114999999998,173.079680999999994 -41.297500999999997,173.085784999999987 -41.302222999999998,173.105529999999987 -41.313332000000003,173.168029999999987 -41.316108999999997,173.188873 -41.313332000000003,173.198853000000014 -41.311385999999999,173.207184000000012 -41.308608999999997,173.272217000000012 -41.274445,173.278320000000008 -41.269722000000002,173.32607999999999 -41.222496,173.340515000000011 -41.205832999999998,173.351623999999987 -41.195549,173.377166999999986 -41.178055,173.426085999999998 -41.150275999999998,173.598021999999986 -41.053612,173.606628 -41.052498,173.639709000000011 -41.073616,173.672760000000011 -41.070557,173.720519999999993 -41.060279999999999,173.727753000000007 -41.056389000000003,173.739959999999996 -41.047226000000002,173.744689999999991 -41.032501000000003,173.743286000000012 -41.024169999999998,173.738281 -41.018608,173.72744800000001 -41.019447,173.718841999999995 -41.022499000000003,173.696930000000009 -41.033614999999998,173.690796000000006 -41.038338000000003,173.682189999999991 -41.041114999999998,173.676085999999998 -41.036391999999999,173.673584000000005 -41.029167,173.674682999999987 -41.020836000000003,173.678314 -41.014449999999997,173.696625000000012 -41.000281999999999,173.751373 -40.976944000000003,173.800262000000004 -40.969161999999997,173.913299999999992 -40.931671,173.984679999999997 -40.896949999999997,173.994415000000004 -40.896667,174.02276599999999 -40.913055,174.027771 -40.91861,174.030272999999994 -40.925834999999999,174.03054800000001 -40.935271999999998,174.02941899999999 -40.943610999999997,174.022217000000012 -40.947220000000002,173.929961999999989 -40.992226000000002,173.832184000000012 -40.995002999999997,173.821075000000008 -40.994163999999998,173.782195999999999 -41.009171000000002,173.778594999999996 -41.015555999999997,173.769713999999993 -41.09861,173.778594999999996 -41.114449,173.794433999999995 -41.109444000000003,173.822204999999997 -41.080832999999998,173.821899000000002 -41.062218,173.836638999999991 -41.055,173.846343999999988 -41.053055,173.93081699999999 -41.049446000000003,173.946625000000012 -41.055832000000002,173.953033000000005 -41.060555,173.917755 -41.087502,173.887482000000006 -41.103332999999999,173.848846000000009 -41.144165,173.820526 -41.245834000000002,173.763306 -41.267502,173.76080300000001 -41.273055999999997,173.775817999999987 -41.289726000000002,173.788025000000005 -41.290557999999997,173.806365999999997 -41.289444000000003,174.03720100000001 -41.218330000000002,174.09970100000001 -41.198334000000003,174.128845000000013 -41.187218,174.133605999999986 -41.181671,174.133605999999986 -41.176108999999997,174.126068000000004 -41.174171,174.008605999999986 -41.175277999999999,174.001099000000011 -41.178885999999999,174.001373 -41.186385999999999,174.000274999999988 -41.194716999999997,173.993010999999996 -41.198334000000003,173.981902999999988 -41.199440000000003,173.932738999999998 -41.199997000000003,173.908324999999991 -41.199997000000003,173.899719000000005 -41.197495000000004,173.893585000000002 -41.192771999999998,173.887206999999989 -41.169724000000002,173.88580300000001 -41.161667,173.888031000000012 -41.154167,173.89776599999999 -41.133614,173.902466000000004 -41.128334000000002,174.032745000000006 -40.999724999999998,174.084411999999986 -41.021385000000002,174.094116000000014 -41.023330999999999,174.246338000000009 -41.044724000000002,174.258330999999998 -41.035277999999998,174.299408 -41.003616,174.315246999999999 -41.000557,174.323853000000014 -41.003334000000002,174.323853000000014 -41.010834000000003,174.321625000000012 -41.018059,174.209960999999993 -41.197495000000004,174.161376999999987 -41.225555,174.152771 -41.226661999999997,174.114685000000009 -41.231383999999998,174.052185000000009 -41.235000999999997,174.02664200000001 -41.236114999999998,174.053864000000004 -41.254173000000002,174.206359999999989 -41.269447,174.214966000000004 -41.266663,174.291655999999989 -41.238892,174.303588999999988 -41.229720999999998,174.320800999999989 -41.220275999999998,174.326904000000013 -41.222771000000002,174.288574000000011 -41.276947,174.249114999999989 -41.322502,174.241637999999995 -41.326110999999997,174.231902999999988 -41.328055999999997,174.208587999999992 -41.329445,174.193572999999986 -41.320281999999999,174.155243000000013 -41.305,174.148041000000006 -41.308608999999997,174.111633000000012 -41.336661999999997,174.052764999999994 -41.421112,174.049133000000012 -41.427779999999998,174.044433999999995 -41.442497000000003,174.045806999999996 -41.450836000000002,174.049988000000013 -41.466392999999997,174.063873 -41.493057,174.084136999999998 -41.526108,174.097473000000008 -41.519447,174.096069 -41.511116,174.100799999999992 -41.505561999999998,174.108306999999996 -41.509171000000002,174.114685000000009 -41.513893000000003,174.125792999999987 -41.523887999999999,174.151093000000003 -41.551392,174.176085999999998 -41.578612999999997,174.178864000000004 -41.586112999999997,174.179961999999989 -41.594444000000003,174.179137999999995 -41.602783000000002,174.174133000000012 -41.608336999999999,174.163299999999992 -41.618606999999997,174.157195999999999 -41.623328999999998,174.193024000000008 -41.685555,174.287749999999988 -41.734444000000003,174.291655999999989 -41.740836999999999,174.289153999999996 -41.748336999999999,174.283324999999991 -41.762222,174.236358999999993 -41.837218999999997,174.212188999999995 -41.865279999999998,174.201355000000007 -41.875557,174.184143000000006 -41.890555999999997,174.17804000000001 -41.895004,174.164703000000003 -41.90361,174.157195999999999 -41.907218999999998,174.143585000000002 -41.915832999999999,174.088561999999996 -41.957779000000002,174.009154999999993 -42.027495999999999,173.979674999999986 -42.061110999999997,173.965239999999994 -42.086945,173.959136999999998 -42.100838000000003,173.954407000000003 -42.115555,173.953307999999993 -42.123885999999999,173.953583000000009 -42.133330999999998,173.956359999999989 -42.166389000000002,173.933319000000012 -42.196944999999999,173.928314 -42.202499000000003,173.884154999999993 -42.243614,173.87802099999999 -42.248055,173.870513999999986 -42.251944999999999,173.86300700000001 -42.255561999999998,173.834686000000005 -42.271385000000002,173.801361000000014 -42.293059999999997,173.794983000000002 -42.297500999999997,173.568572999999986 -42.476944000000003,173.558593999999999 -42.487777999999999,173.541350999999992 -42.511947999999997,173.533874999999995 -42.525002,173.531372000000005 -42.532218999999998,173.500823999999994 -42.599724000000002,173.480804000000006 -42.621941,173.466063999999989 -42.656944000000003,173.459960999999993 -42.670836999999999,173.448853000000014 -42.690277000000002,173.387755999999996 -42.793616999999998,173.351623999999987 -42.840836000000003,173.328033000000005 -42.898887999999999,173.328033000000005 -42.906387000000002,173.325806 -42.913612,173.285521999999986 -42.958053999999997,173.106902999999988 -43.057777000000002,173.099396000000013 -43.061385999999999,173.090515000000011 -43.064444999999999,173.026366999999993 -43.081947,172.975799999999992 -43.09111,172.953033000000005 -43.092773,172.935516000000007 -43.098334999999999,172.92025799999999 -43.105834999999999,172.838012999999989 -43.148055999999997,172.818024000000008 -43.160828000000002,172.797760000000011 -43.183059999999998,172.772217000000012 -43.219718999999998,172.76080300000001 -43.239165999999997,172.758026 -43.246665999999998,172.726944000000003 -43.415539000000003,172.726593000000008 -43.423355,172.775817999999987 -43.612220999999998,172.894135000000006 -43.61972,172.90554800000001 -43.620834000000002,173.059692000000013 -43.653053,173.100525000000005 -43.697220000000002,173.105529999999987 -43.703612999999997,173.114409999999992 -43.741385999999999,173.116913000000011 -43.762504999999997,173.116913000000011 -43.769996999999996,173.112182999999987 -43.825004999999997,173.110779000000008 -43.833328000000002,173.091644000000002 -43.856392,173.058593999999999 -43.871108999999997,173.006088000000005 -43.869498999999998,173.002242999999993 -43.868999000000002,172.999236999999994 -43.865668999999997,172.990905999999995 -43.849670000000003,172.965239999999994 -43.802222999999998,172.961365 -43.764449999999997,172.958862000000011 -43.756950000000003,172.951355000000007 -43.755279999999999,172.939972000000012 -43.756110999999997,172.927185000000009 -43.766112999999997,172.921906000000007 -43.771667,172.920532000000009 -43.824173000000002,172.920532000000009 -43.833328000000002,172.917572000000007 -43.869221000000003,172.923584000000005 -43.875884999999997,172.931243999999992 -43.881390000000003,172.937408000000005 -43.887886000000002,172.938904000000008 -43.892384,172.938247999999987 -43.896220999999997,172.870789000000002 -43.904442000000003,172.861633000000012 -43.901389999999999,172.811919999999986 -43.882216999999997,172.804413000000011 -43.878334000000002,172.745789000000002 -43.837775999999998,172.740783999999991 -43.832222000000002,172.736908 -43.825836000000002,172.715239999999994 -43.808334000000002,172.642761000000007 -43.772224,172.513031000000012 -43.729438999999999,172.495238999999998 -43.723885000000003,172.483582000000013 -43.722771000000002,172.472197999999992 -43.72361,172.424988000000013 -43.733612,172.413299999999992 -43.743614,172.390258999999986 -43.763893000000003,172.384978999999987 -43.77861,172.382445999999987 -43.795006,172.383330999999998 -43.8125,172.384704999999997 -43.820838999999999,172.394713999999993 -43.850281000000003,172.393311000000011 -43.858612,172.386932000000002 -43.863334999999999,172.367461999999989 -43.867775000000002,172.297211000000004 -43.881110999999997,172.286925999999994 -43.883057,172.275542999999999 -43.883887999999999,172.189696999999995 -43.909163999999997,172.050812000000008 -43.965279000000002,171.978577 -43.995834000000002,171.955230999999998 -44.006667999999998,171.941070999999994 -44.015006999999997,171.782470999999987 -44.076949999999997,171.654694000000006 -44.122498,171.583587999999992 -44.153885000000002,171.547211000000004 -44.173889000000003,171.538483000000014 -44.178775999999999,171.355559999999997 -44.285491999999998,171.346465999999992 -44.292186999999998,171.342467999999997 -44.295555,171.319976999999994 -44.316391000000003,171.293578999999994 -44.343612999999998,171.285521999999986 -44.356392,171.278594999999996 -44.369995000000003,171.271911999999986 -44.383887999999999,171.269135000000006 -44.391112999999997,171.275542999999999 -44.395836000000003,171.279144000000002 -44.402495999999999,171.278594999999996 -44.420836999999999,171.277190999999988 -44.428885999999999,171.275542999999999 -44.437218,171.26998900000001 -44.451942000000003,171.263306 -44.465553,171.254974000000004 -44.478332999999999,171.214966000000004 -44.533057999999997,171.208313000000004 -44.537506,171.200531000000012 -44.541114999999998,171.196075000000008 -44.560279999999999,171.192200000000014 -44.645836000000003,171.193024000000008 -44.663330000000002,171.20495600000001 -44.700279000000002,171.21414200000001 -44.739165999999997,171.215239999999994 -44.747498,171.207184000000012 -44.851112,171.198577999999998 -44.919998,171.195800999999989 -44.927498,171.185241999999988 -44.938332000000003)))" - } - }, - { - "pk": 1, - "model": "geoapp.city", - "fields": { - "name": "Houston", - "point": "SRID=4326;POINT (-95.363151 29.763374)" - } - }, - { - "pk": 2, - "model": "geoapp.city", - "fields": { - "name": "Dallas", - "point": "SRID=4326;POINT (-96.801611 32.782057)" - } - }, - { - "pk": 3, - "model": "geoapp.city", - "fields": { - "name": "Oklahoma City", - "point": "SRID=4326;POINT (-97.521157 34.464642)" - } - }, - { - "pk": 4, - "model": "geoapp.city", - "fields": { - "name": "Wellington", - "point": "SRID=4326;POINT (174.783117 -41.315268)" - } - }, - { - "pk": 5, - "model": "geoapp.city", - "fields": { - "name": "Pueblo", - "point": "SRID=4326;POINT (-104.609252 38.255001)" - } - }, - { - "pk": 6, - "model": "geoapp.city", - "fields": { - "name": "Lawrence", - "point": "SRID=4326;POINT (-95.235060 38.971823)" - } - }, - { - "pk": 7, - "model": "geoapp.city", - "fields": { - "name": "Chicago", - "point": "SRID=4326;POINT (-87.650175 41.850385)" - } - }, - { - "pk": 8, - "model": "geoapp.city", - "fields": { - "name": "Victoria", - "point": "SRID=4326;POINT (-123.305196 48.462611)" - } - }, - { - "pk": 1, - "model": "geoapp.state", - "fields": { - "name": "Colorado", - "poly": "SRID=4326;POLYGON ((-107.9184209999999800 41.0020359999999970, -107.6913358243141800 41.0021042503422490, -107.6256240000000000 41.0021240000000020, -107.5215053632723100 41.0025067105257720, -107.3674429999999900 41.0030730000000010, -107.3177944624011200 41.0029672133671140, -107.3053129562196700 41.0029406188977600, -107.2411939999999900 41.0028039999999980, -107.0006060000000000 41.0034439999999950, -106.8577729999999900 41.0026629999999910, -106.4538589999999900 41.0020569999999940, -106.4395630000000100 41.0019780000000010, -106.4374190000000100 41.0017949999999940, -106.4309500000000000 41.0017519999999960, -106.3918520000000000 41.0011760000000010, -106.3863560000000100 41.0011439999999960, -106.3211649999999900 40.9991229999999970, -106.2175730000000000 40.9977340000000010, -106.1946242545105400 40.9976261471179200, -106.1905405794911800 40.9976069549524670, -106.0611809999999900 40.9969990000000020, -105.7642468572674100 40.9968975561793200, -105.7304210000000100 40.9968860000000030, -105.7248040000000100 40.9969100000000000, -105.5544177044212800 40.9973907108230620, -105.4122207031675800 40.9977918911954480, -105.2771379999999800 40.9981730000000010, -105.2565270000000100 40.9981909999999980, -105.2547790000000000 40.9982100000000000, -105.1734357616781100 40.9981770152523170, -104.9433706805682100 40.9980837236793720, -104.8552730000000000 40.9980479999999970, -104.8295039999999900 40.9992700000000030, -104.6759990000000000 41.0009570000000000, -104.4971489999999900 41.0018280000000030, -104.4970580000000000 41.0018049999999970, -104.4676719999999800 41.0014729999999970, -104.2146920000000000 41.0016570000000020, -104.2141910000000000 41.0015679999999990, -104.2114730000000000 41.0015909999999980, -104.1235859999999900 41.0016259999999950, -104.1045900000000000 41.0015429999999910, -104.0860679999999800 41.0015629999999970, -104.0669609999999900 41.0015039999999970, -104.0532489999999900 41.0014059999999960, -104.0392380000000000 41.0015020000000020, -104.0233829999999800 41.0018870000000040, -104.0182230000000100 41.0016170000000030, -104.0108048867797200 41.0016166745085400, -103.9726419999999800 41.0016150000000010, -103.9713730000000000 41.0015240000000030, -103.9535250000000000 41.0015959999999990, -103.9063240000000000 41.0013870000000010, -103.8962070000000000 41.0017500000000010, -103.8779670000000000 41.0016729999999900, -103.8584489999999900 41.0016809999999980, -103.7504979999999900 41.0020540000000010, -103.5745220000000000 41.0017210000000030, -103.4974469999999900 41.0016350000000000, -103.4866970000000100 41.0019139999999990, -103.4219750000000000 41.0020069999999990, -103.4219250000000000 41.0019689999999950, -103.3969909999999900 41.0025580000000010, -103.3653139999999800 41.0018460000000000, -103.3629790000000000 41.0018439999999910, -103.0778040000000000 41.0022980000000030, -103.0765360000000000 41.0022530000000030, -103.0595379999999900 41.0023679999999970, -103.0579980000000000 41.0023679999999970, -103.0434439999999900 41.0023440000000010, -103.0387040000000000 41.0022510000000010, -103.0020260000000000 41.0024859999999980, -103.0001019999999800 41.0024000000000020, -102.9826900000000100 41.0021569999999970, -102.9814830000000000 41.0021119999999970, -102.9636689999999800 41.0021859999999950, -102.9625220000000100 41.0020719999999980, -102.9607060000000000 41.0020589999999960, -102.9596239999999900 41.0020949999999970, -102.9448300000000000 41.0023029999999980, -102.9431090000000100 41.0020510000000020, -102.9255680000000000 41.0022799999999990, -102.9240290000000000 41.0021419999999990, -102.9065469999999900 41.0022759999999950, -102.9047960000000000 41.0022069999999990, -102.8874069999999800 41.0021780000000010, -102.8857460000000000 41.0021309999999990, -102.8678220000000000 41.0021830000000020, -102.8657839999999900 41.0019879999999970, -102.8492629999999900 41.0023010000000030, -102.8464550000000100 41.0022560000000030, -102.8303029999999900 41.0023509999999970, -102.8272800000000000 41.0021429999999970, -102.7735460000000000 41.0024140000000020, -102.7667230000000000 41.0022749999999900, -102.7546170000000000 41.0023609999999930, -102.7396239999999900 41.0022299999999970, -102.6534629999999900 41.0023320000000030, -102.6210330000000000 41.0025970000000020, -102.5786959999999900 41.0022910000000000, -102.5757380000000000 41.0022680000000010, -102.5754960000000000 41.0022000000000020, -102.5660479999999900 41.0022000000000020, -102.5567889999999900 41.0022189999999970, -102.5177010715824500 41.0023473358779430, -102.4879549999999900 41.0024449999999940, -102.4705369999999800 41.0023819999999970, -102.4692230000000000 41.0024239999999980, -102.4603345936969100 41.0024118023655500, -102.3877509894849600 41.0023121952773270, -102.3803729919328400 41.0023020703894620, -102.3795930000000000 41.0023010000000030, -102.3640659999999900 41.0021739999999970, -102.2928330000000000 41.0022069999999990, -102.2926219999999900 41.0022299999999970, -102.2925530000000000 41.0022069999999990, -102.2913540000000000 41.0022069999999990, -102.2778034555739900 41.0022337435695480, -102.2720999999999900 41.0022450000000020, -102.2678119999999900 41.0023830000000020, -102.2319310000000000 41.0023270000000010, -102.2121999999999800 41.0024620000000010, -102.2093610000000000 41.0024420000000020, -102.2090839362510100 41.0024402293320020, -102.2077760551977500 41.0024318708833140, -102.1912100000000000 41.0023259999999960, -102.1249720000000000 41.0023380000000020, -102.0705980000000000 41.0024230000000000, -102.0516140000000000 41.0023770000000030, -102.0512919999999900 40.7495910000000020, -102.0516344326070000 40.5821295926172280, -102.0517250000000000 40.5378389999999910, -102.0515190000000000 40.5200940000000000, -102.0514649999999900 40.4400079999999990, -102.0518400000000000 40.3963960000000030, -102.0515719999999900 40.3930799999999980, -102.0517979999999900 40.3600690000000030, -102.0515855426165400 40.3506461457410240, -102.0513090000000000 40.3383809999999980, -102.0519220000000000 40.2353439999999980, -102.0518939999999900 40.2291929999999950, -102.0519089999999900 40.1626740000000030, -102.0520010000000000 40.1483589999999990, -102.0518526008886700 40.0644696175368220, -102.0517440000000000 40.0030780000000020, -102.0517155646578700 39.9781730274562650, -102.0515690000000000 39.8498050000000030, -102.0513629999999900 39.8434710000000010, -102.0513179999999900 39.8333110000000020, -102.0512540000000000 39.8189920000000010, -102.0505939999999900 39.6755939999999900, -102.0500990000000000 39.6538120000000020, -102.0504219999999800 39.6460479999999930, -102.0499540000000000 39.5923310000000010, -102.0498059999999900 39.5740580000000010, -102.0495539999999900 39.5389320000000030, -102.0496730000000000 39.5366909999999980, -102.0496790000000000 39.5061830000000000, -102.0493690000000000 39.4233330000000000, -102.0493700000000000 39.4182100000000020, -102.0491669999999800 39.4035969999999980, -102.0489599999999800 39.3737119999999900, -102.0484490000000100 39.3031379999999900, -102.0472500000000100 39.1370200000000000, -102.0471339999999900 39.1297009999999970, -102.0465709999999900 39.0470380000000010, -102.0453879999999900 38.8133919999999930, -102.0453340000000000 38.7994630000000030, -102.0454479999999900 38.7834530000000020, -102.0453710000000000 38.7700639999999980, -102.0452870000000000 38.7555279999999980, -102.0453750000000100 38.7543389999999950, -102.0452119999999900 38.6975669999999990, -102.0451560000000100 38.6885550000000010, -102.0451269999999900 38.6867250000000030, -102.0451600000000000 38.6752210000000010, -102.0451020000000000 38.6749459999999980, -102.0450740000000000 38.6696170000000020, -102.0452880000000000 38.6152489999999990, -102.0452109999999900 38.5816089999999930, -102.0451889999999800 38.5587319999999920, -102.0452229999999900 38.5437969999999980, -102.0451120000000000 38.5237839999999990, -102.0452619999999900 38.5055319999999950, -102.0452629999999900 38.5053950000000000, -102.0453239999999800 38.4536469999999970, -102.0449360000000100 38.4196800000000000, -102.0444419999999900 38.4158019999999990, -102.0449440000000000 38.3844190000000010, -102.0446130000000000 38.3123239999999970, -102.0445680000000000 38.2688190000000010, -102.0443980000000000 38.2500149999999980, -102.0442510000000000 38.1417779999999950, -102.0445353236236500 38.1276753800285280, -102.0445890000000000 38.1250129999999960, -102.0445402773320500 38.1232621932310920, -102.0442550000000100 38.1130109999999930, -102.0446235449346800 38.0490802965374900, -102.0446310070721800 38.0477858554663160, -102.0446440000000100 38.0455320000000010, -102.0446200858831200 38.0420217065706370, -102.0445393420861100 38.0301695264644910, -102.0438440000000100 37.9281019999999960, -102.0438450000000000 37.9261350000000020, -102.0432189999999900 37.8679289999999900, -102.0430329999999900 37.8241459999999990, -102.0429530000000000 37.8035349999999970, -102.0426680000000100 37.7887580000000010, -102.0421580000000000 37.7601640000000030, -102.0418760000000000 37.7238750000000000, -102.0415740000000000 37.6804360000000000, -102.0416940000000100 37.6656809999999990, -102.0415819999999900 37.6544949999999970, -102.0415850000000000 37.6442819999999970, -102.0416180000000000 37.6078680000000030, -102.0417794058394900 37.5786915552958260, -102.0418940000000000 37.5579770000000010, -102.0418990000000000 37.5411860000000030, -102.0420159999999900 37.5352609999999980, -102.0417860000000000 37.5060659999999970, -102.0418009999999900 37.4694879999999980, -102.0417549999999900 37.4348549999999920, -102.0416690000000000 37.4347399999999980, -102.0416759999999800 37.4098979999999910, -102.0415240000000000 37.3750179999999970, -102.0420890000000000 37.3528189999999970, -102.0419740000000000 37.3526129999999980, -102.0418169999999900 37.3094899999999970, -102.0416640000000000 37.2976499999999900, -102.0419630000000000 37.2581640000000010, -102.0420020000000000 37.1417440000000030, -102.0421350000000000 37.1250209999999970, -102.0420920000000000 37.1250209999999970, -102.0418089999999900 37.1119729999999990, -102.0419830000000000 37.1065510000000030, -102.0419199999999900 37.0350830000000000, -102.0417490000000000 37.0343969999999980, -102.0419210000000000 37.0321780000000020, -102.0419500000000000 37.0308050000000010, -102.0419519999999900 37.0247419999999960, -102.0422400000000100 36.9930829999999990, -102.0545030000000000 36.9931089999999970, -102.1842710000000000 36.9935929999999970, -102.2083160000000000 36.9937299999999990, -102.2607890000000000 36.9943880000000010, -102.2703456818352900 36.9943999333374620, -102.3075936504973800 36.9944464445206690, -102.3552880000000000 36.9945059999999940, -102.3553670000000000 36.9945749999999980, -102.6981420000000000 36.9951489999999980, -102.7420599999999800 36.9976890000000010, -102.7598600000000000 37.0000190000000020, -102.7785689999999900 36.9992420000000020, -102.8067620000000100 37.0000190000000020, -102.8146160000000000 37.0007829999999980, -102.8419890000000000 36.9995979999999990, -102.9796130000000000 36.9985489999999970, -102.9858069999999900 36.9985709999999980, -102.9869760000000000 36.9985240000000030, -103.0021990000000000 37.0001040000000000, -103.0861049694139500 37.0001738656940380, -103.1054053653286500 37.0001899364881130, -103.1559220000000000 37.0002319999999970, -103.2006317284721000 37.0000603865096880, -103.3271777694062600 36.9995746531243130, -103.4256788724330500 36.9991965672206930, -103.7332470000000100 36.9980159999999930, -103.7343639999999900 36.9980410000000010, -104.0078549999999900 36.9962390000000030, -104.2154754884639700 36.9948744321965890, -104.2505359999999800 36.9946440000000010, -104.2975706429598400 36.9940532503817470, -104.3388329999999900 36.9935350000000010, -104.3556505253503100 36.9935565317715810, -104.3664476855351100 36.9935703555644370, -104.3992028820832900 36.9936122926149620, -104.4297693866500100 36.9936514274448880, -104.4806103169752700 36.9937165199763950, -104.5192570000000000 36.9937659999999940, -104.6245560000000000 36.9943769999999930, -104.6255450000000000 36.9935990000000030, -104.6450289999999900 36.9933779999999930, -104.7061122072671500 36.9934264441886570, -104.7320310000000100 36.9934470000000030, -104.7321199999999800 36.9934840000000020, -104.8399904125201100 36.9933955928282070, -105.0005539999999800 36.9932640000000030, -105.0292279999999900 36.9927289999999900, -105.1208000000000000 36.9954279999999970, -105.1550419641015100 36.9953391471581630, -105.2206130000000000 36.9951689999999970, -105.2512960000000000 36.9956049999999980, -105.4193100000000000 36.9958560000000030, -105.4381027436132300 36.9959680306975970, -105.4424590000000000 36.9959940000000030, -105.4472550000000000 36.9960169999999950, -105.4651820000000000 36.9959909999999970, -105.4708767069258500 36.9959784767062560, -105.5088359999999900 36.9958949999999900, -105.5124850000000000 36.9957769999999970, -105.5339220000000000 36.9958749999999980, -105.6274699999999900 36.9956790000000030, -105.6647199999999900 36.9958740000000010, -105.7164710000000000 36.9958489999999930, -105.7184073045491700 36.9958460161492080, -105.9961590000000100 36.9954180000000010, -105.9974720000000000 36.9954170000000030, -106.0066339999999900 36.9953429999999980, -106.0998058674331200 36.9947591067049760, -106.1639713822180900 36.9943569916150140, -106.2014689999999900 36.9941219999999970, -106.2477050000000000 36.9942660000000030, -106.2486750000000100 36.9942879999999900, -106.2932790000000000 36.9938900000000000, -106.3254286788247500 36.9941092316646730, -106.3431389999999800 36.9942300000000020, -106.4762779528519600 36.9938393350510210, -106.5005890000000100 36.9937679999999960, -106.6171590000000000 36.9929670000000000, -106.6171249999999900 36.9930039999999990, -106.6286520000000000 36.9931750000000010, -106.6287329999999800 36.9931609999999940, -106.6613440000000000 36.9932430000000000, -106.6756259999999900 36.9931229999999970, -106.7505910000000000 36.9924609999999990, -106.7820952897146500 36.9924517499673660, -106.8697959999999900 36.9924259999999950, -106.8772919999999800 37.0001389999999900, -106.9188864633427000 37.0001287471619240, -106.9560183645763800 37.0001195943242540, -107.1072626267445100 37.0000823133304520, -107.2552027234696400 37.0000458467977750, -107.4209130000000000 37.0000049999999940, -107.4224150136806600 37.0000049896361030, -107.4421819991443200 37.0000048532438730, -107.4817370013986600 37.0000045803142900, -107.5240868464170200 37.0000042881002860, -107.6007136407778000 37.0000037593752680, -107.7124779008385600 37.0000029882016790, -107.8556954995019300 37.0000020000000020, -107.8663089376175300 37.0000019267672610, -107.8691399085863600 37.0000019072335460, -107.8691806993063300 37.0000019069521000, -108.0006230000000000 37.0000009999999970, -108.1791870752784600 36.9992931616249270, -108.1871395402988700 36.9992616375912750, -108.2493580000000000 36.9990150000000000, -108.2506349999999900 36.9995610000000000, -108.2880860000000100 36.9995550000000010, -108.2884000000000000 36.9995199999999970, -108.3204640000000000 36.9994990000000000, -108.3207209999999900 36.9995100000000010, -108.3791655704288800 36.9994589777070430, -108.6196889999999900 36.9992489999999990, -108.6203090000000100 36.9992870000000020, -108.7492698254058500 36.9991399338227680, -108.9544040000000000 36.9989059999999980, -108.9588680000000000 36.9989130000000020, -109.0452229999999900 36.9990840000000030, -109.0451659999999800 37.0727419999999980, -109.0450580000000000 37.0746609999999990, -109.0449950000000000 37.0864290000000030, -109.0451889999999900 37.0962709999999940, -109.0451730000000100 37.1094640000000030, -109.0452029999999900 37.1119579999999940, -109.0451559999999900 37.1120639999999970, -109.0459950000000000 37.1772789999999990, -109.0459780000000100 37.2018309999999990, -109.0458015051062800 37.2050708135988660, -109.0454869999999900 37.2108440000000020, -109.0455598795331900 37.2397756720027080, -109.0455601977475200 37.2399019965338450, -109.0455839999999900 37.2493509999999970, -109.0460389999999900 37.2499930000000030, -109.0458980422733100 37.3269349905485300, -109.0458100000000000 37.3749930000000030, -109.0437991323256200 37.4690283342420190, -109.0434637832608200 37.4847104508718160, -109.0434249427800400 37.4865267700853340, -109.0431370000000000 37.4999919999999990, -109.0419150000000000 37.5306530000000010, -109.0418650000000000 37.5307260000000010, -109.0418059999999900 37.6041710000000010, -109.0421310000000000 37.6176620000000030, -109.0420890000000000 37.6237950000000010, -109.0422690000000000 37.6660669999999980, -109.0417320000000000 37.7112139999999980, -109.0417600000000000 37.7131820000000030, -109.0416360000000000 37.7402099999999980, -109.0420980000000000 37.7499899999999970, -109.0420421489000700 37.7543839997998490, -109.0414610000000000 37.8001050000000020, -109.0417540000000000 37.8358259999999900, -109.0417229999999900 37.8420509999999980, -109.0418440000000000 37.8727880000000000, -109.0416528185789500 37.8811669027885570, -109.0410580000000100 37.9072359999999970, -109.0431209999999900 37.9742600000000010, -109.0428189999999900 37.9970679999999990, -109.0428199999999900 37.9993010000000030, -109.0419724039658400 38.1317991668174590, -109.0418366569747100 38.1530194495367920, -109.0417619999999900 38.1646900000000000, -109.0546480000000000 38.2449209999999980, -109.0600619999999900 38.2754890000000000, -109.0599620000000000 38.4999869999999900, -109.0602530000000000 38.5993279999999930, -109.0595410000000000 38.7198879999999970, -109.0573880000000000 38.7954559999999940, -109.0572160449167600 38.7997308495970760, -109.0541889999999900 38.8749839999999980, -109.0539429999999900 38.9044140000000030, -109.0537970000000000 38.9052839999999950, -109.0532330000000100 38.9424670000000010, -109.0532919999999800 38.9428780000000000, -109.0524359999999900 38.9999850000000020, -109.0515879125035400 39.1157342577700790, -109.0515835157382700 39.1163343400931040, -109.0515807806367500 39.1167076340895430, -109.0515120000000000 39.1260949999999990, -109.0507650000000000 39.3666770000000030, -109.0513629999999800 39.4976740000000040, -109.0510402483332400 39.6604720118441210, -109.0506149999999900 39.8749699999999980, -109.0508730000000000 40.0589149999999990, -109.0508130000000100 40.0595789999999920, -109.0509440000000000 40.1807119999999930, -109.0509730000000000 40.1808490000000020, -109.0509687158226500 40.2226624122704100, -109.0509460000000000 40.4443679999999970, -109.0503140000000000 40.4950920000000000, -109.0506979999999800 40.4999630000000010, -109.0499550000000000 40.5399010000000000, -109.0500740000000000 40.5403579999999980, -109.0500719640466700 40.5404371043087370, -109.0480440000000000 40.6192309999999990, -109.0482490000000000 40.6536009999999950, -109.0490880000000000 40.7145620000000010, -109.0484550000000000 40.8260810000000020, -109.0500760000000000 41.0006589999999990, -108.8841379999999900 41.0000939999999970, -108.6311080000000000 41.0001559999999900, -108.5266670000000000 40.9996080000000020, -108.5006590000000000 41.0001120000000010, -108.2506490000000000 41.0001140000000040, -108.1812270000000100 41.0004549999999950, -108.0892190031385500 41.0015541392473680, -108.0465390000000000 41.0020639999999970, -107.9232341223959400 41.0020370519008000, -107.9184209999999800 41.0020359999999970))" - } - }, - { - "pk": 2, - "model": "geoapp.state", - "fields": { - "name": "Kansas", - "poly": "SRID=4326;POLYGON ((-102.051743999999999 40.003078000000002,-101.916696000000002 40.003141999999997,-101.904176000000007 40.003162000000003,-101.861740775252272 40.002907997451267,-101.841025000000002 40.002783999999991,-101.832160999999999 40.002932999999999,-101.807687 40.002797999999999,-101.804861999999986 40.002752,-101.783266423091504 40.002735966476642,-101.748492901329186 40.002710149056902,-101.627071 40.00262,-101.625809000000004 40.002710999999998,-101.579641080501318 40.002654627564297,-101.54227299999998 40.002608999999993,-101.417209 40.002423999999998,-101.411050023557621 40.002364583193085,-101.409952999999987 40.002353999999997,-101.387325211262052 40.00246006676732,-101.374325999999996 40.002520999999994,-101.342859000000004 40.002580000000002,-101.324035999999992 40.002695999999993,-101.293991000000005 40.002558999999998,-101.286555000000007 40.002558999999998,-101.248672999999997 40.002543000000003,-101.215033000000005 40.002555,-101.19221899999998 40.002490999999999,-101.186892990733696 40.002481867883319,-101.178804999999997 40.002468,-101.168704000000005 40.002547,-101.130906999999993 40.002426999999997,-101.104950380573442 40.002382874850099,-101.104950380567658 40.002382874850092,-101.060316999999998 40.002307000000002,-101.027686000000003 40.002255999999996,-100.962089380046322 40.00217532965339,-100.937427 40.002144999999999,-100.764559455237134 40.002296963384197,-100.758829999999989 40.002301999999993,-100.752183000000002 40.002127999999992,-100.7388309887763 40.002228385746484,-100.733295999999996 40.002270000000003,-100.729904000000005 40.002110999999999,-100.721127999999993 40.002068999999999,-100.683435000000003 40.002234,-100.660229999999984 40.002161999999998,-100.645444999999995 40.001882999999999,-100.626504856699498 40.001892789287538,-100.600944999999996 40.001905999999998,-100.594756999999987 40.001976999999997,-100.567238000000003 40.001888999999998,-100.551885999999996 40.001888999999998,-100.514429898999168 40.001844039098764,-100.511064999999988 40.00184,-100.487159000000005 40.001767,-100.477018 40.001752000000003,-100.475853999999998 40.001767999999998,-100.468772999999985 40.001724000000003,-100.447071999999991 40.001795,-100.439081000000002 40.001773999999997,-100.402449685831499 40.001800164690437,-100.390079999999998 40.001809000000002,-100.290126736823893 40.001691651381378,-100.231651999999997 40.001623000000002,-100.229478999999998 40.001692999999996,-100.215406000000002 40.001629,-100.196958999999993 40.001494,-100.19359 40.001573,-100.190323000000006 40.001586000000003,-100.188181 40.001541000000003,-100.177823000000004 40.001593,-99.990926000000002 40.001503,-99.986610999999996 40.001550000000002,-99.948166999999998 40.001812999999999,-99.944417 40.001584,-99.930432999999979 40.001516000000002,-99.906657999999993 40.001511999999998,-99.850154757615712 40.001444140609848,-99.813400999999999 40.001399999999997,-99.775639999999981 40.001646999999998,-99.772120999999984 40.001804,-99.764213999999996 40.001550999999999,-99.756834999999995 40.001342,-99.746628 40.001820000000002,-99.737774855821357 40.001824224692157,-99.731959000000003 40.001826999999992,-99.719639 40.001807999999997,-99.628345999999993 40.001866,-99.625979999999984 40.001865000000002,-99.515340365578794 40.002008435606839,-99.501791999999995 40.002026,-99.498998999999984 40.00195699999999,-99.497659999999982 40.001911999999997,-99.493464999999986 40.001936999999998,-99.480727999999999 40.001942,-99.423564999999996 40.002270000000003,-99.412644999999998 40.001867999999995,-99.403389000000004 40.001969000000003,-99.366747146128873 40.001962496644857,-99.290702999999979 40.001949000000003,-99.286655999999994 40.002017000000002,-99.282966999999999 40.001879000000002,-99.254012000000003 40.002074,-99.25036999999999 40.00195699999999,-99.216375999999997 40.002015999999998,-99.197591999999986 40.002032999999997,-99.188905000000005 40.002023,-99.186961999999994 40.001976999999997,-99.178965000000005 40.001976999999997,-99.169815999999997 40.001925,-99.123032999999992 40.002164999999998,-99.113510000000005 40.002192999999998,-99.085596999999993 40.002132999999994,-99.067047050731574 40.00217023690761,-99.020337999999995 40.00226399999999,-99.018700999999993 40.002333,-98.992135000000005 40.002192,-98.972286999999994 40.002245000000002,-98.971721000000002 40.002268,-98.961009000000004 40.002316999999998,-98.96091899999999 40.002271,-98.953888072580213 40.002253239016738,-98.934791999999987 40.002204999999996,-98.8435956659672 40.002348607685946,-98.842134005327992 40.002350909376077,-98.834455999999989 40.002363000000003,-98.820589999999982 40.002319,-98.777203 40.002358999999998,-98.774940999999998 40.002336,-98.729330606936671 40.002229113826232,-98.726294999999993 40.002222000000003,-98.710403999999997 40.002180000000003,-98.693095999999997 40.002372999999999,-98.691443000000007 40.002504999999999,-98.690286999999998 40.002547999999997,-98.672819000000004 40.002364,-98.669723999999988 40.002409999999998,-98.653832999999992 40.002268999999998,-98.652494000000004 40.002245000000002,-98.640709999999984 40.002493,-98.616372379820518 40.002409030470169,-98.613754999999998 40.002400000000002,-98.593342000000007 40.002475999999994,-98.575219000000004 40.002479999999991,-98.560578000000007 40.002274,-98.543186000000006 40.002285,-98.523053000000004 40.002336,-98.506634585466017 40.002329436673158,-98.504454999999979 40.002328565375151,-98.50084801790743 40.002327123469641,-98.490532999999999 40.002322999999997,-98.441997554675297 40.002366263566756,-98.391848301995964 40.002410965650498,-98.274015000000006 40.002516,-98.268218000000005 40.002490000000002,-98.25000799999998 40.002307000000002,-98.213290432425865 40.002506421375415,-98.193483 40.002614,-98.179315000000003 40.002482999999998,-98.172269 40.002437999999991,-98.156873151934732 40.002445128178877,-98.142031000000003 40.002451999999998,-98.099659000000003 40.002226999999998,-98.076034000000007 40.002300999999996,-98.068701000000004 40.002355,-98.050056999999981 40.002277999999997,-98.047469000000007 40.002186000000002,-98.042767978165202 40.002191261754177,-98.014411999999979 40.002223,-98.010157000000007 40.002153,-97.972185999999994 40.002113999999999,-97.931810999999996 40.002049999999997,-97.876261 40.002102,-97.85745 40.002065000000002,-97.838378999999989 40.001909999999995,-97.821597999999994 40.002003999999992,-97.819426000000007 40.001957999999995,-97.777154999999993 40.002167,-97.770775999999998 40.001976999999997,-97.769204000000002 40.001995,-97.767746000000002 40.001994000000003,-97.714825952022949 40.001974503868432,-97.706952472912022 40.001971603221314,-97.701160424072398 40.001969469388285,-97.61370959051284 40.001937251863488,-97.595964207410262 40.001930714334961,-97.593671575073259 40.001929869712491,-97.51530799999999 40.001900999999997,-97.511381 40.001899000000002,-97.510264000000006 40.001835,-97.48896951184885 40.0019310946697,-97.481234094494141 40.001966001936331,-97.463284999999999 40.00204699999999,-97.444661999999994 40.001957999999995,-97.425443 40.002048000000002,-97.417825999999991 40.002023999999999,-97.415833000000006 40.002000999999993,-97.369102999999996 40.00206,-97.350896000000006 40.001930000000002,-97.350272000000004 40.001975999999999,-97.256541388870858 40.001563097676062,-97.24516899999999 40.001513000000003,-97.245080000000002 40.001466999999998,-97.202309999999997 40.00144199999999,-97.200190000000006 40.001548999999997,-97.181775000000002 40.001550000000002,-97.165136038800085 40.001526729909067,-97.164288093539042 40.001525544031956,-97.157705785150654 40.001516338474417,-97.144150390621661 40.001497380844818,-97.142447999999987 40.001494999999998,-97.137865999999988 40.001814000000003,-97.049662999999995 40.001322999999999,-97.030802999999992 40.001342,-97.009164999999996 40.001463,-96.918187714380807 40.001505032225388,-96.916093000000004 40.001505999999999,-96.880459000000002 40.001448000000003,-96.878253 40.001466,-96.875056999999998 40.001448000000003,-96.873812 40.001449999999998,-96.868892342573133 40.001444286089438,-96.805301797100199 40.001370429180717,-96.693246065483095 40.001240282633297,-96.622400999999996 40.001157999999997,-96.610348999999999 40.000881,-96.604883999999984 40.000891000000003,-96.581788013355691 40.000963078853125,-96.580851999999993 40.000965999999998,-96.570853999999997 40.001090999999995,-96.557862999999998 40.000968,-96.538977000000003 40.000850999999997,-96.527110999999991 40.001030999999998,-96.469944999999996 40.000965999999998,-96.467535999999996 40.001035000000002,-96.463639999999998 40.000967000000003,-96.354812913341547 40.000735780492882,-96.350953725714731 40.00072758106856,-96.341811057717251 40.000708156095854,-96.304554999999993 40.000629000000004,-96.301066000000006 40.000632000000003,-96.239171999999996 40.000691000000003,-96.223838999999998 40.000729,-96.220170999999993 40.00072,-96.154364999999984 40.000495,-96.154246 40.00045,-96.147166999999982 40.000478999999999,-96.125936999999979 40.000432000000004,-96.125788 40.000467,-96.089781000000002 40.000518999999997,-96.081394999999986 40.000602999999998,-96.051691000000005 40.000726999999998,-96.02409 40.000718999999997,-96.014716997898134 40.000662392993569,-96.010677999999999 40.000638000000002,-95.995389500007406 40.000603953777215,-95.958139000000003 40.000520999999999,-95.901560435593936 40.000482839492363,-95.882524000000004 40.00047,-95.788023999999993 40.000452000000003,-95.784575000000004 40.000463000000003,-95.672891827756033 40.000336669594915,-95.658762261555324 40.000320686938032,-95.63965170807073 40.000299070038061,-95.565216789143676 40.000214872989645,-95.463103327781397 40.00009936736172,-95.455130658279359 40.000090349077695,-95.452048066669519 40.000086862204618,-95.438655850967621 40.000071713601649,-95.436011808705814 40.000068722793607,-95.426269559489981 40.00005770284973,-95.42148745056565 40.000052293567869,-95.41484447973599 40.000044779372317,-95.375257000000005 40.0,-95.339895999999982 39.999999000000003,-95.326448603970363 39.999998574530281,-95.30829 39.999997999999998,-95.308403999999996 39.993758,-95.307779999999994 39.990617999999998,-95.307111000000006 39.989114,-95.302506999999991 39.984357000000003,-95.289715 39.977705999999998,-95.274756999999994 39.972115000000002,-95.269885999999985 39.969396000000003,-95.261854 39.960617999999997,-95.257651999999993 39.954886000000002,-95.250253999999998 39.948644000000002,-95.241382999999999 39.944949,-95.236761 39.943930999999999,-95.231114000000005 39.943784,-95.220212000000004 39.944432999999997,-95.216440000000006 39.943953,-95.213736999999995 39.943205999999996,-95.204427999999993 39.938949,-95.201277000000005 39.934193999999998,-95.200689999999994 39.928154999999997,-95.20201 39.922438,-95.205744999999993 39.915168999999999,-95.206326000000004 39.912120999999999,-95.206195999999991 39.909557,-95.205732999999981 39.908275000000003,-95.202631127107011 39.904826841138963,-95.201935000000006 39.904052999999998,-95.19982446963148 39.902956959499498,-95.199773390195176 39.902930432929807,-95.199730682369619 39.902908253904485,-95.199347000000003 39.902709000000002,-95.193815999999984 39.900689999999997,-95.189565000000002 39.899959000000003,-95.179452999999995 39.900061999999991,-95.172296000000003 39.902025999999999,-95.159833999999989 39.906984,-95.156023999999988 39.907243,-95.153185481864639 39.906665666721331,-95.151701132402337 39.906363761184394,-95.149656999999991 39.905947999999995,-95.148243978290679 39.905255611516672,-95.146055000000004 39.904183000000003,-95.143801999999994 39.901917999999995,-95.142562999999981 39.897992000000002,-95.142444999999995 39.895419999999994,-95.143403000000006 39.889355999999992,-95.142718000000002 39.885888999999992,-95.140601000000004 39.88168799999999,-95.137091999999996 39.878350999999995,-95.134747000000004 39.876852,-95.128165999999993 39.874164999999998,-95.105912000000004 39.869163999999998,-95.090958483530287 39.863446088154532,-95.090158000000002 39.86314,-95.085003 39.861882999999999,-95.081534000000005 39.861718000000003,-95.079786043666886 39.861878094210859,-95.052535000000006 39.864373999999998,-95.042141999999998 39.864804999999997,-95.037767000000002 39.865541999999998,-95.034318031759739 39.867229060943565,-95.032053000000005 39.868336999999997,-95.031002514960235 39.869148692103728,-95.027930999999981 39.871521999999999,-95.025918959417425 39.87568321107333,-95.025422000000006 39.876711,-95.025119000000004 39.878833,-95.02586205726584 39.885935119809076,-95.025947000000002 39.886747,-95.025771840138887 39.887478608302466,-95.025475064075451 39.888718183571669,-95.025239999999997 39.889699999999998,-95.024388999999999 39.891202,-95.021897459019584 39.893924778577606,-95.019088516216428 39.896994416745422,-95.018742999999986 39.89737199999999,-95.018224596337973 39.897611313155366,-95.013151999999991 39.899952999999996,-95.010684182587667 39.900289758615472,-95.008439999999993 39.900596,-95.003818999999993 39.900400999999995,-94.990284000000003 39.897010000000002,-94.989784913598129 39.896958718834505,-94.987823822157509 39.896757216540813,-94.986975 39.89667,-94.985308455245701 39.896814869812808,-94.979390624873346 39.897329296428744,-94.977748999999989 39.897472,-94.963345000000004 39.901136,-94.959276000000003 39.901671,-94.958440298615102 39.901548064610132,-94.95153999999998 39.900532999999996,-94.943866999999983 39.898130000000002,-94.935306395585215 39.893779379194363,-94.934493000000003 39.893366,-94.929574000000002 39.888753999999992,-94.927897000000002 39.88611199999999,-94.927358999999996 39.883966,-94.927251999999996 39.880257999999991,-94.928466 39.876344000000003,-94.931084167287821 39.873075003673321,-94.931462999999994 39.872602,-94.938790999999995 39.866954,-94.940742999999998 39.864409999999999,-94.942407000000003 39.861065999999994,-94.942566999999983 39.856601999999995,-94.939767000000003 39.851930000000003,-94.937655000000007 39.849786000000002,-94.926149999999993 39.841321999999998,-94.916917999999995 39.836137999999998,-94.909941999999987 39.834426,-94.903156999999979 39.833849999999998,-94.892677000000006 39.834377999999994,-94.889493000000002 39.834026,-94.886932999999999 39.833098,-94.881012999999996 39.828921999999991,-94.878676999999996 39.826521999999997,-94.877043999999998 39.823754,-94.876543999999996 39.820594,-94.875944000000004 39.813293999999999,-94.87632679569893 39.807169268817198,-94.876344000000003 39.806894,-94.876705972386588 39.80614007495069,-94.880932 39.797338000000003,-94.884084 39.794234000000003,-94.890292000000002 39.791626,-94.892965000000004 39.791097999999991,-94.912908274541266 39.790276806342419,-94.92474489359239 39.789789416146199,-94.925605000000004 39.789753999999995,-94.925940683661906 39.789631963361245,-94.927258709273417 39.789152799691166,-94.929653999999999 39.788281999999995,-94.93035270914757 39.787827111232048,-94.930442023078356 39.787768964141691,-94.930979872832452 39.787418801541357,-94.932726000000002 39.786282,-94.932913349551853 39.786043884763131,-94.935059195246907 39.783316584105528,-94.935205999999994 39.78313,-94.935782000000003 39.778905999999999,-94.935301999999993 39.77561,-94.935036741896766 39.775108050050804,-94.934624304242575 39.774327591105198,-94.934262000000004 39.773642000000002,-94.929652999999988 39.769098,-94.929421387607832 39.768921584953624,-94.928626586797606 39.768316199289764,-94.926229000000006 39.76648999999999,-94.923991330811134 39.765173947104167,-94.916788999999994 39.760937999999996,-94.912293000000005 39.759337999999993,-94.906244 39.759417999999997,-94.905921454535701 39.759501730763866,-94.900921941190049 39.760799572828766,-94.899156000000005 39.761257999999998,-94.899025621234756 39.761323457651685,-94.895268000000002 39.76321,-94.895041000000006 39.763350000000003,-94.894070999999997 39.763945999999997,-94.893918999999997 39.76404,-94.893724000000006 39.764159999999997,-94.893646000000004 39.764208000000004,-94.883923999999993 39.770186000000002,-94.881460000000004 39.771258000000003,-94.881422 39.771258000000003,-94.874706321154179 39.772392308082928,-94.871144 39.772993999999997,-94.869643999999994 39.772894,-94.867142999999999 39.771693999999997,-94.865243000000007 39.770094,-94.863142999999994 39.767294,-94.860742999999999 39.763094000000002,-94.859442999999999 39.753694000000003,-94.860369889104035 39.749534984666809,-94.860371 39.74953,-94.862942999999987 39.742994000000003,-94.870142999999999 39.734594,-94.875642999999997 39.730494,-94.884142999999995 39.726793999999998,-94.891743999999989 39.724893999999999,-94.899315999999999 39.724041999999997,-94.900553640444031 39.724102079633198,-94.901582061073228 39.724152002964722,-94.902612000000005 39.724201999999998,-94.906055088082866 39.724933471502588,-94.907014260749904 39.725137244236571,-94.907785666538786 39.725301126582274,-94.910067999999995 39.725785999999999,-94.911087398893699 39.726157408899255,-94.918323999999998 39.728793999999994,-94.93000499999998 39.735370000000003,-94.939221000000003 39.74157799999999,-94.944740999999979 39.744377,-94.948658825773634 39.745572502168315,-94.948680784417533 39.745579202723142,-94.948725999999994 39.745593,-94.952629999999999 39.745961,-94.955286 39.745688999999999,-94.960086000000004 39.743065,-94.963779819772341 39.740240978767318,-94.964692434782606 39.73954326086956,-94.965317999999982 39.739064999999989,-94.965565209393333 39.738728671232884,-94.970224508542714 39.732389687437191,-94.970421999999985 39.732120999999999,-94.971205999999981 39.729304999999997,-94.971078000000006 39.723146,-94.968452999999997 39.707402000000002,-94.968980999999999 39.692954,-94.969909 39.689050000000002,-94.971316999999999 39.686410000000002,-94.976096392737759 39.68160006801152,-94.976325000000003 39.68137,-94.981556999999995 39.678634000000002,-94.984149000000002 39.677849999999992,-94.986445080691666 39.677537608069159,-94.990587390516794 39.676974028501114,-94.993556999999996 39.676569999999998,-94.998766892420107 39.676509388876212,-95.001379 39.676479,-95.004602740773237 39.676177881356345,-95.008105239221649 39.675850724907868,-95.009022999999999 39.675764999999998,-95.010225986948086 39.675477408241932,-95.013209713780839 39.674764104372102,-95.015309999999985 39.674261999999999,-95.018317999999994 39.672868999999999,-95.021521942351953 39.670631293568427,-95.024595000000005 39.668484999999997,-95.027643999999995 39.665453999999997,-95.037464 39.652904999999997,-95.039049000000006 39.649639,-95.044554000000005 39.644370000000002,-95.049518000000006 39.637875999999999,-95.053366999999994 39.630347,-95.054924999999997 39.624994999999991,-95.055024819926729 39.623527163368095,-95.055152000000007 39.621656999999992,-95.054958456554544 39.620961328886679,-95.053131423647116 39.614394255464305,-95.053011999999981 39.613965,-95.052555891387243 39.613278556984888,-95.047910999999999 39.606287999999999,-95.046445000000006 39.601605999999997,-95.046361000000005 39.599556999999997,-95.047165000000007 39.595117000000002,-95.049277000000004 39.589582999999998,-95.054804000000004 39.582487999999998,-95.056897000000006 39.580567000000002,-95.059518999999995 39.579132,-95.064519000000004 39.577114999999999,-95.066275551338904 39.576786470694117,-95.068266439890465 39.576414113098046,-95.069315000000003 39.576217999999997,-95.072159999999982 39.576121999999991,-95.075842551355763 39.576644128527029,-95.076688000000004 39.576763999999997,-95.088569515113861 39.580713698327401,-95.089515000000006 39.581028000000003,-95.09068217809525 39.58095107619048,-95.095736000000002 39.580618,-95.099095000000005 39.579690999999997,-95.100375015351446 39.579100080742663,-95.10182940536555 39.578428661399109,-95.103228 39.577782999999997,-95.106109962410699 39.575487768136732,-95.106244982404746 39.575380236480051,-95.106274453941239 39.575356764970024,-95.106406000000007 39.575251999999999,-95.107454000000004 39.573842999999997,-95.108931052511622 39.56997896968771,-95.109155613535791 39.569391508783276,-95.112830712234896 39.559777298955126,-95.112877844119652 39.559653999999995,-95.113077000000004 39.559132999999996,-95.113262990573148 39.557121201967142,-95.113516735173974 39.554376531201555,-95.113557 39.553940999999995,-95.113330390574859 39.553319942050457,-95.109694540741444 39.543355336910984,-95.109303999999995 39.542284999999993,-95.106595999999996 39.537657000000003,-95.106363136618171 39.537386330858773,-95.106309238275287 39.537323682029822,-95.102887999999993 39.533346999999999,-95.096713422649174 39.527826015970483,-95.095319522838992 39.526579663685382,-95.092703999999998 39.524241000000004,-95.082713999999996 39.516711999999998,-95.077440999999993 39.513551999999997,-95.059460999999985 39.506143000000002,-95.05637999999999 39.503971999999997,-95.052176999999986 39.499995999999996,-95.050551999999996 39.497514000000002,-95.049844999999991 39.494414999999989,-95.048370000000006 39.480420000000002,-95.047133000000002 39.474970999999996,-95.045715999999999 39.472459,-95.040779999999998 39.466386999999997,-95.03749999999998 39.463689000000002,-95.033407999999994 39.460875999999992,-95.028497999999999 39.458286999999999,-95.015825000000007 39.452809000000002,-94.995767999999998 39.448174000000002,-94.990172 39.446192000000003,-94.982144000000005 39.440551999999997,-94.978797999999998 39.436241000000003,-94.976606000000004 39.426701,-94.972952000000006 39.421705000000003,-94.968547467194298 39.418879728230785,-94.966981164652054 39.417875029083376,-94.966065999999998 39.417287999999999,-94.965430539155946 39.417093446959996,-94.954817000000006 39.413843999999997,-94.951209000000006 39.411706999999993,-94.947863999999996 39.408603999999997,-94.946292999999983 39.405645999999997,-94.946662000000003 39.399717000000003,-94.946226999999993 39.395648,-94.945577 39.393850999999998,-94.942038999999994 39.389499,-94.939696999999995 39.387949999999996,-94.938205659338905 39.38711651736979,-94.938199994953891 39.387113351650086,-94.938198708293527 39.387112632559479,-94.937157999999982 39.386530999999998,-94.933651999999995 39.385545999999998,-94.932170703908099 39.385397898493565,-94.923109999999994 39.384492000000002,-94.919224999999997 39.385173999999999,-94.915858999999998 39.386347999999998,-94.909581000000003 39.388865000000003,-94.901822999999993 39.392797999999999,-94.896448416922993 39.39340032396553,-94.894979000000006 39.393565000000002,-94.891845000000004 39.393312999999999,-94.888971999999995 39.392431999999999,-94.888058227506249 39.391822741147728,-94.885025999999996 39.389800999999999,-94.880978999999996 39.383899,-94.879281000000006 39.37977999999999,-94.879087999999996 39.375702999999994,-94.881359999999987 39.370382999999997,-94.885216 39.366911000000002,-94.890928000000002 39.364030999999997,-94.896832000000003 39.363135,-94.899023999999997 39.362431,-94.902496999999997 39.360382999999999,-94.90716104958679 39.35683832231404,-94.907297 39.356735,-94.907510470967722 39.356484333333356,-94.909408999999997 39.354254999999995,-94.910016999999996 39.352542999999997,-94.91007929967077 39.352122876579188,-94.910166635146069 39.351533921963672,-94.910234386079424 39.351077037464393,-94.910640999999998 39.348334999999999,-94.910055514481613 39.342727430625168,-94.908683540921714 39.329587162119807,-94.908064999999993 39.323663000000003,-94.906599414484603 39.317389801180319,-94.905982468129508 39.31474906332776,-94.905548856768945 39.312893060899633,-94.905328999999995 39.311951999999998,-94.904604536115073 39.31007473956825,-94.903592654247163 39.307452709910535,-94.903137 39.306272,-94.902341179156608 39.304705098857582,-94.900693838394318 39.301461629999167,-94.900362755471093 39.300809756886117,-94.900048999999996 39.300192000000003,-94.895217000000002 39.29420799999999,-94.889432625546704 39.288730528394183,-94.887056 39.286479999999997,-94.882576 39.283327999999997,-94.881425972827699 39.282735692772164,-94.879585687759075 39.281787876778168,-94.878319999999988 39.281135999999996,-94.869470269042495 39.278423959123423,-94.867567999999991 39.277841000000002,-94.866576416802303 39.277461598502107,-94.857072000000002 39.273825000000002,-94.850573678793964 39.270595179638669,-94.850470600187833 39.270543947117169,-94.846320000000006 39.268481,-94.844203868589133 39.266965084952687,-94.840994595225254 39.264666085108807,-94.837855000000005 39.262416999999999,-94.836102527271095 39.260730409704507,-94.834879550067882 39.259553409087879,-94.832501320107042 39.257264586268427,-94.832102355305352 39.256880620143498,-94.831470999999993 39.256273,-94.827487000000005 39.249888999999996,-94.826571473591798 39.245793223963304,-94.825663000000006 39.241728999999999,-94.826110999999983 39.238289000000002,-94.826405091238243 39.237538367125239,-94.827108391663188 39.2357432765168,-94.827791000000005 39.234000999999999,-94.828117769197533 39.233533772937683,-94.83244435695299 39.227347452739551,-94.834698551534771 39.224124319346686,-94.834896 39.223841999999998,-94.834917496024659 39.223414229109203,-94.835039139715306 39.220993519665384,-94.835046603087861 39.220844998551456,-94.835055999999994 39.220658,-94.833551999999997 39.217793999999998,-94.831678999999994 39.215938,-94.823791 39.209873999999999,-94.820687000000007 39.208626000000002,-94.811662999999996 39.206594000000003,-94.800359054554335 39.206051410618606,-94.799662999999995 39.206018,-94.798924306591658 39.206116812235138,-94.793913945984244 39.206787029303406,-94.788135068391796 39.207560047994349,-94.787343000000007 39.207666000000003,-94.783838000000003 39.207154000000003,-94.781518000000005 39.206145999999997,-94.780294078498926 39.205273290755756,-94.778971577769184 39.204330290235411,-94.777838000000003 39.203522,-94.775537999999997 39.200602000000003,-94.770897097034492 39.191141697801093,-94.770859400196969 39.191064854247678,-94.770337999999995 39.190002,-94.763137999999998 39.179903000000003,-94.75233799999998 39.173202999999994,-94.74193799999999 39.170203,-94.736536999999998 39.169203000000003,-94.723636999999997 39.169002999999989,-94.714136999999994 39.170403,-94.696331999999998 39.178562999999997,-94.687235999999999 39.183503000000002,-94.680335999999983 39.184303,-94.669134999999997 39.182003000000002,-94.663835000000006 39.179102999999998,-94.660314999999997 39.168050999999991,-94.662434999999988 39.157602999999995,-94.650734999999983 39.154102999999999,-94.640034999999997 39.153102999999994,-94.623934000000006 39.156602999999997,-94.615834000000007 39.160003000000003,-94.608834000000002 39.160502999999999,-94.601732999999996 39.15960299999999,-94.596033000000006 39.157702999999998,-94.591932999999997 39.155003,-94.589933000000002 39.140402999999999,-94.592533000000003 39.135902999999992,-94.600433999999993 39.128503000000002,-94.605733999999998 39.122204000000004,-94.607033999999999 39.119404000000003,-94.607354 39.113444,-94.607234000000005 39.089604,-94.607333999999994 39.081703999999995,-94.607275518421304 39.072346947409343,-94.607234000000005 39.065703999999997,-94.607342812841125 39.057404745686583,-94.60738379566331 39.05427894858029,-94.607437239929951 39.050202705779334,-94.607518499034526 39.044004999999999,-94.607559479691474 39.040879368039988,-94.607612854426819 39.036808428453341,-94.60764748233197 39.034167326647299,-94.607654028188094 39.033668068249817,-94.60769507925167 39.030537066312085,-94.607898722639391 39.015005000000002,-94.607914652765047 39.013789994834404,-94.607991843462372 39.007902590176151,-94.608087526605189 39.000604749887366,-94.608092759229436 39.000205652880325,-94.608190956218394 38.992716079262479,-94.608212339138063 38.991085184540211,-94.608264438806202 38.98711149548862,-94.608279957450691 38.985927874365181,-94.608301211867541 38.984306780669819,-94.608333999999999 38.981805999999999,-94.608294286905348 38.97390309416464,-94.608273484679671 38.969763451253407,-94.608249262127771 38.964943163424849,-94.608207879518076 38.956708024096379,-94.608134000000007 38.942005999999999,-94.608134000000007 38.940005999999997,-94.607866 38.937398000000002,-94.607977999999989 38.936869999999992,-94.608002838442587 38.912906322207348,-94.608006216770249 38.909646973100827,-94.608010407202343 38.90560412040071,-94.608022064178897 38.894357681354862,-94.60802273861259 38.893706999999999,-94.608033000000006 38.883806999999997,-94.608033000000006 38.869207000000003,-94.608033000000006 38.868107000000002,-94.607992999999993 38.867271000000002,-94.608033000000006 38.861207,-94.608033000000006 38.855007,-94.608033000000006 38.850107,-94.608033000000006 38.847206999999997,-94.607624999999999 38.827559999999991,-94.607668973053535 38.825816299300072,-94.608041 38.811064000000002,-94.608617609754887 38.781926100280451,-94.609398999999996 38.742440000000002,-94.60945599999998 38.74069999999999,-94.609507757106428 38.73815999467709,-94.609625656590424 38.7323740198146,-94.610322172686637 38.698192151600097,-94.61071084056907 38.679118085101081,-94.61073851283922 38.677760054904219,-94.611602000000005 38.635384000000002,-94.611464999999995 38.625011,-94.611857999999998 38.620485000000002,-94.611908 38.609271999999997,-94.611905562149673 38.605890005062435,-94.611886999999996 38.580139000000003,-94.611902 38.580109999999998,-94.612176000000005 38.576546,-94.612156999999996 38.549816999999997,-94.612272000000004 38.547916999999998,-94.612464742072774 38.518760616499996,-94.612644000000003 38.491644,-94.612725999999995 38.484366999999999,-94.612696 38.483153999999999,-94.612865999999997 38.477570999999998,-94.613365000000002 38.403421999999999,-94.613264999999998 38.392426,-94.613275404637193 38.388718047420433,-94.613328999999993 38.369617999999996,-94.613311999999993 38.364407,-94.613085455228386 38.3436360393057,-94.612999999999985 38.335800999999996,-94.612825 38.324387000000002,-94.612787999999995 38.320141999999997,-94.612673 38.314832000000003,-94.612673 38.302526999999998,-94.612689368276676 38.301464114946214,-94.612843999999996 38.291422999999995,-94.612848999999997 38.289914000000003,-94.612707829046002 38.272362044447966,-94.612691999999996 38.270394000000003,-94.612613999999994 38.237766,-94.612634999999997 38.226987,-94.612658999999994 38.219251,-94.61265866950464 38.21872154645218,-94.612657999999982 38.217649000000002,-94.612821999999994 38.203918000000002,-94.612848 38.200713999999998,-94.613073 38.190551999999997,-94.61341774677895 38.168183959706163,-94.613422 38.16790799999999,-94.613748 38.160632999999997,-94.613855999999998 38.149768999999999,-94.613889466360249 38.136312911169284,-94.613919497343275 38.124238112111762,-94.614061000000007 38.067343,-94.614088999999993 38.065900999999997,-94.614054999999993 38.060088,-94.613980999999995 38.036949,-94.614211999999981 37.992462000000003,-94.614464999999996 37.987798999999995,-94.614511123427164 37.979395512107736,-94.614514360688489 37.978805697169918,-94.614557000000005 37.971036999999995,-94.614561999999992 37.951517000000003,-94.614593999999997 37.949977999999994,-94.614611999999994 37.944361999999998,-94.614754000000005 37.940769000000003,-94.614834999999999 37.936700000000002,-94.614778 37.934199999999997,-94.615181000000007 37.915944000000003,-94.615392999999983 37.906391999999997,-94.61546899999999 37.901775,-94.615706000000003 37.886842999999999,-94.615921 37.878331000000003,-94.615834000000007 37.872509999999998,-94.616 37.863126,-94.616282760205394 37.851281931678621,-94.616426000000004 37.845281999999997,-94.616433230053673 37.842955730230052,-94.61645 37.837560000000003,-94.616861999999998 37.819456000000002,-94.6176681324215 37.775831003788085,-94.617720999999989 37.77297,-94.617737774720197 37.764628336555312,-94.617744979193901 37.761045725680312,-94.617807999999997 37.729706999999998,-94.617975 37.722175999999997,-94.61780499999999 37.690178000000003,-94.617650999999995 37.687671000000002,-94.617687000000004 37.686652999999993,-94.617885 37.682214000000002,-94.617733999999999 37.673127,-94.617576 37.653671000000003,-94.617517909436387 37.643988652624088,-94.61747699999998 37.637169999999998,-94.617472819127315 37.636539916507168,-94.6173 37.610495,-94.617428000000004 37.609521999999998,-94.617283 37.571896000000002,-94.617315000000005 37.571498999999996,-94.617080999999999 37.567013000000003,-94.61711240153133 37.563155381499406,-94.617159999999984 37.557307999999999,-94.617186000000004 37.553485000000002,-94.617167532949537 37.551779056391354,-94.616988484457494 37.535238968895172,-94.616907999999981 37.527804000000003,-94.616788999999997 37.521509999999999,-94.616880369657522 37.506771761864755,-94.617022999999989 37.483764999999998,-94.617182999999997 37.469664999999999,-94.617180000000005 37.465203000000002,-94.617221999999998 37.460476,-94.617204999999984 37.46037299999999,-94.617200999999994 37.454788,-94.617131999999998 37.439818000000002,-94.617265000000003 37.425535999999994,-94.617510999999993 37.410908999999997,-94.617557000000005 37.396374999999999,-94.61759158917468 37.381725975861251,-94.617625000000004 37.367576,-94.617626 37.367444999999989,-94.617536999999999 37.364355000000003,-94.617636000000005 37.338417,-94.617694999999998 37.336841999999997,-94.617648000000003 37.323588999999998,-94.61807499999999 37.240436000000003,-94.618157999999994 37.237596999999994,-94.618122999999997 37.229334,-94.61815 37.228121000000002,-94.618218999999996 37.207771999999999,-94.618305000000007 37.207337000000003,-94.618319 37.188774000000002,-94.618504999999985 37.181184000000002,-94.618472999999994 37.174782,-94.618351000000004 37.160210999999997,-94.618071999999998 37.132345,-94.61807499999999 37.129755000000003,-94.61816441512984 37.118929895303054,-94.618212 37.113168999999992,-94.618150999999997 37.103968000000002,-94.618059000000002 37.096676000000002,-94.618088 37.093670999999993,-94.618089999999995 37.093494,-94.618085255103864 37.089305442941992,-94.618082 37.086432000000002,-94.61811999999999 37.085934000000002,-94.617981999999998 37.075077,-94.617954311863897 37.070346986543974,-94.617947706480578 37.069218577181687,-94.617910161110728 37.062804634983031,-94.617893975984089 37.060039701061122,-94.617874999999998 37.056798,-94.617877538275181 37.056339390079508,-94.617964999999998 37.040537,-94.617972200190749 37.032971759572447,-94.617994999999979 37.009015999999995,-94.618027702303792 37.004829720380044,-94.618080000000006 36.998134999999998,-94.625223999999989 36.998671999999999,-94.699735000000004 36.998804999999997,-94.701796999999999 36.998814000000003,-94.711286363060864 36.99879670415919,-94.712770000000006 36.99879399999999,-94.722629184733563 36.998741903378082,-94.732834603466486 36.998687977231512,-94.736330645670293 36.998669503899912,-94.737183000000002 36.998665000000003,-94.739323999999996 36.998686999999997,-94.745992043692311 36.998700535427304,-94.749560781475736 36.99870777958963,-94.749834600019099 36.998708335412474,-94.777257000000006 36.998764,-94.831280000000007 36.998812,-94.831539962039543 36.998812565955092,-94.840925999999996 36.998832999999998,-94.846637705524785 36.998860673615503,-94.849800999999999 36.998875999999996,-94.853196999999994 36.998874,-94.896852168601683 36.999075231107383,-94.904605402657381 36.99911097010289,-94.9407882710602 36.999277757196154,-94.995293000000004 36.999528999999995,-95.007620000000003 36.999513999999998,-95.011432999999997 36.999535000000002,-95.030323999999993 36.999516999999997,-95.037857000000002 36.999496999999998,-95.049498999999983 36.999580000000002,-95.073509 36.999509000000003,-95.1405498515593 36.999533623834409,-95.155186999999998 36.999538999999999,-95.155372 36.999540000000003,-95.159067907556633 36.999536629205572,-95.177300999999986 36.999519999999997,-95.195307 36.999564999999997,-95.267905305061163 36.999446910377756,-95.285983101167361 36.999417504731007,-95.322564999999997 36.999357999999994,-95.328057999999999 36.999364999999997,-95.328326999999987 36.999366000000002,-95.331209999999999 36.999380000000002,-95.377252424658963 36.999296311678272,-95.381753803085687 36.999288129815376,-95.407683000000006 36.999240999999998,-95.511578 36.999234999999999,-95.522414951940931 36.999281058114107,-95.534401000000003 36.999332000000003,-95.573598000000004 36.999309999999994,-95.601517312742303 36.999317968253862,-95.612139999999997 36.999321000000002,-95.615933999999996 36.999364999999997,-95.621011943860609 36.999361983160732,-95.624350000000007 36.999360000000003,-95.630078999999995 36.999319999999997,-95.638122589416028 36.999320470082949,-95.664300999999995 36.999321999999999,-95.674044398562955 36.999333876292773,-95.686452000000003 36.999349000000002,-95.696658999999997 36.999215,-95.71038 36.999370999999996,-95.710782524132014 36.999362783399121,-95.714887000000004 36.999279,-95.718053999999995 36.999254999999998,-95.741907999999995 36.999243999999997,-95.746466244276931 36.999250838506164,-95.759905000000003 36.999270999999993,-95.768719000000004 36.999205000000003,-95.786761999999996 36.999309999999994,-95.807979999999986 36.999124000000002,-95.819364496908179 36.999150471530008,-95.866899000000004 36.999260999999997,-95.873943999999995 36.999299999999998,-95.875256999999991 36.999302,-95.877150999999998 36.999304000000002,-95.910179999999997 36.999336,-95.928122000000002 36.999245000000002,-95.936991999999989 36.999267999999994,-95.964270378114222 36.999093604402042,-96.00081 36.99886,-96.095164142267308 36.998935940299688,-96.14121 36.998972999999999,-96.143207000000004 36.999133999999998,-96.147143 36.999021999999997,-96.149709 36.99904,-96.152383999999998 36.999050999999994,-96.154016999999982 36.999161,-96.184768000000005 36.999211000000003,-96.200028000000003 36.999028000000003,-96.217571000000007 36.999070000000003,-96.235320025971774 36.999130675786525,-96.27480962492784 36.999265672629733,-96.276368000000005 36.999270999999993,-96.279078999999996 36.999271999999991,-96.381556872639493 36.999226629434901,-96.394272 36.999220999999999,-96.415412000000003 36.999113,-96.500287999999998 36.998643,-96.525212323286397 36.998711038495294,-96.525495980566973 36.998711812823821,-96.551130926932458 36.998781791180214,-96.551130927020779 36.998781791180456,-96.624487489749981 36.998982040153741,-96.705431000000004 36.999203,-96.710481999999999 36.999270999999993,-96.731983006833033 36.99928335311408,-96.736589999999993 36.999285999999998,-96.741269999999986 36.999239000000003,-96.749837999999997 36.998987999999997,-96.792060000000006 36.999179999999996,-96.795198999999997 36.99886,-96.822790999999995 36.999181999999998,-96.867517000000007 36.999217000000002,-96.876289999999997 36.999232999999997,-96.902083000000005 36.999155000000002,-96.903509999999997 36.999132000000003,-96.917092999999994 36.999181999999998,-96.921914999999998 36.999150999999998,-96.924934279563146 36.999131784030439,-96.934641999999997 36.999070000000003,-96.967371 36.999066999999997,-96.975561999999996 36.999018999999997,-97.030081999999979 36.998928999999997,-97.039783999999983 36.999000000000002,-97.045562883576096 36.99899981011751,-97.100651999999997 36.998998,-97.104275999999999 36.999020000000002,-97.120284999999996 36.999014000000003,-97.122596999999999 36.999035999999997,-97.147721000000004 36.999110999999999,-97.202000013134437 36.999050609464689,-97.255829185181881 36.998990719420135,-97.342808466604069 36.998893946743877,-97.362376892203329 36.998872175019791,-97.364929972745273 36.99886933447624,-97.372421000000003 36.998860999999998,-97.384924999999996 36.998843,-97.462279999999993 36.998685000000002,-97.472860999999995 36.998721000000003,-97.527292000000003 36.998749999999994,-97.545899999999989 36.998708999999991,-97.546497999999985 36.998746999999995,-97.564536000000004 36.998711,-97.582460039051355 36.99869862770732,-97.606549 36.998682000000002,-97.637136999999996 36.999090000000002,-97.650465999999994 36.999003999999999,-97.692180007144742 36.998844793059916,-97.697103999999982 36.998826,-97.768704 36.998749999999994,-97.783432000000005 36.998961,-97.783489000000003 36.998846999999998,-97.802297999999979 36.998713000000002,-97.965379045838461 36.998468720211747,-98.033955000000006 36.998365999999997,-98.03989 36.998348999999997,-98.045341999999991 36.998327000000003,-98.111985000000004 36.998133000000003,-98.128185436781749 36.99814624647324,-98.147452 36.998162,-98.177595999999994 36.998008999999996,-98.190367496841162 36.998003995168112,-98.208218000000002 36.997996999999998,-98.219498999999999 36.997824,-98.237712000000002 36.99797199999999,-98.346187999999984 36.997962,-98.347186011230875 36.997961873429141,-98.354073 36.997960999999997,-98.408991 36.998513000000003,-98.418267999999998 36.998538000000003,-98.420209 36.998516000000002,-98.476584655565659 36.998733519956424,-98.485495952453391 36.998767903324406,-98.544871999999984 36.998997000000003,-98.622244241689657 36.999025734091177,-98.714511999999999 36.99906,-98.718464999999995 36.999179999999996,-98.761596999999995 36.999425000000002,-98.791936000000007 36.999254999999998,-98.793711000000002 36.999226999999991,-98.797451999999993 36.999228999999993,-98.811832237742053 36.99924038482925,-98.869449000000003 36.999285999999998,-98.880009 36.999262999999999,-98.880579999999995 36.999308999999997,-98.994371 36.999493,-99.000846251360556 36.99951188908193,-99.029336999999998 36.999594999999999,-99.044703416105833 36.999312701167916,-99.049695 36.999220999999999,-99.124882999999997 36.99942,-99.12944899999998 36.999422000000003,-99.215429691089184 36.999525607779709,-99.221470798208784 36.999532887387339,-99.22594390512576 36.999538277535649,-99.248119999999986 36.999564999999997,-99.277506000000002 36.999578999999997,-99.375390999999993 37.000176999999994,-99.375813215185289 37.000169016042221,-99.396673054015949 36.999774562980598,-99.407015 36.999578999999997,-99.456202999999988 36.999471,-99.484333000000007 36.999625999999999,-99.500394999999983 36.99957599999999,-99.500394999999983 36.999637,-99.502664999999993 36.999645,-99.504092999999997 36.999648,-99.508573999999982 36.999657999999997,-99.541115670095081 36.999572526667627,-99.558068000000006 36.999527999999998,-99.625399000000002 36.999670999999999,-99.648651999999998 36.999603999999998,-99.657657999999998 37.000197,-99.675479899119665 37.000294824261658,-99.700804554289192 37.000433831091229,-99.722499410223932 37.000552913981863,-99.774254999999997 37.000836999999997,-99.774816 37.000841,-99.786016000000004 37.000931,-99.875408999999991 37.001658999999997,-99.904897139036905 37.001652107487203,-99.995200999999994 37.001631000000003,-100.001285999999993 37.001699000000002,-100.002562999999995 37.001705999999999,-100.005706000000004 37.001725999999998,-100.08949117994996 37.002091554886341,-100.115721999999991 37.002206,-100.187546999999995 37.002082,-100.19237099999998 37.002035999999997,-100.193753999999998 37.002133,-100.201675999999992 37.002080999999997,-100.269984573601874 37.001795796937508,-100.395563941387664 37.001271475927872,-100.434302684862132 37.001109733298897,-100.434343961694367 37.0011095609592,-100.436759028295029 37.001099477534027,-100.462594151135718 37.000991610310841,-100.526354155903263 37.000725398506596,-100.551597999999998 37.000619999999998,-100.552683000000002 37.000734999999999,-100.591328000000004 37.000376000000003,-100.591413000000003 37.000399000000002,-100.629769999999994 37.000025,-100.63332699999998 36.999935999999998,-100.675551999999996 36.999687999999999,-100.734516999999997 36.999059000000003,-100.756894000000003 36.999356999999996,-100.759718826764541 36.999297806889686,-100.765484 36.999177000000003,-100.806116000000003 36.999091,-100.814277000000004 36.999085,-100.850054835707368 36.998687920265262,-100.855633999999981 36.998626000000002,-100.891659999999987 36.998604,-100.904274 36.998745,-100.904588000000004 36.998561000000002,-100.945565999999985 36.998151999999997,-100.958261085233985 36.99812508251128,-100.996501999999992 36.998044,-101.012641000000002 36.998176,-101.053589000000002 36.997966999999996,-101.066742000000005 36.997920999999998,-101.096255470166753 36.997758490771822,-101.211485999999979 36.997123999999999,-101.212908999999996 36.997044000000002,-101.283204233333976 36.996668964004151,-101.357796999999991 36.996271,-101.359673999999998 36.996231999999999,-101.37818 36.996164,-101.413867999999994 36.996008000000003,-101.415004999999994 36.995966000000003,-101.485326 36.995610999999997,-101.519065999999995 36.99554599999999,-101.555239 36.99541399999999,-101.600396000000003 36.995152999999995,-101.601592999999994 36.995094999999999,-101.672152845644291 36.994768289529276,-101.672152845650672 36.994768289529254,-101.718235173867967 36.994554916342196,-101.761767880686108 36.994353348566563,-101.826607533764985 36.994053124077894,-101.862548907230888 36.993886706153717,-101.881050066963184 36.993801040963412,-101.899294480822903 36.993716564573397,-101.902439999999999 36.993701999999999,-101.905152836625547 36.993689460946754,-101.93521516037174 36.99355050931414,-102.000446999999994 36.993248999999992,-102.000446999999994 36.993271999999997,-102.028206999999981 36.993124999999999,-102.042240000000007 36.993082999999999,-102.041951999999995 37.024741999999996,-102.04195 37.030805,-102.041921000000002 37.032178000000002,-102.041748999999996 37.034396999999998,-102.04191999999999 37.035083,-102.041983000000002 37.106551000000003,-102.041808999999986 37.111972999999999,-102.042091999999997 37.125020999999997,-102.042135000000002 37.125020999999997,-102.042121535383529 37.12671399835564,-102.042001999999997 37.141744000000003,-102.041962999999996 37.258164,-102.041663999999997 37.29764999999999,-102.041816999999995 37.309489999999997,-102.041973999999996 37.352612999999998,-102.042089000000004 37.352818999999997,-102.041523999999995 37.375017999999997,-102.041585760607518 37.389190434149839,-102.041675999999981 37.409897999999991,-102.041668999999999 37.434739999999998,-102.041754999999995 37.434854999999992,-102.041800999999992 37.469487999999998,-102.041786000000002 37.506065999999997,-102.04201599999999 37.535260999999998,-102.041899 37.541186000000003,-102.041893999999999 37.557977,-102.041618 37.607868000000003,-102.041584999999998 37.644281999999997,-102.041581999999991 37.654494999999997,-102.041694000000007 37.665680999999999,-102.041573999999997 37.680436,-102.041876000000002 37.723875,-102.041989965869121 37.73854062916552,-102.042158 37.760164000000003,-102.042668000000006 37.788758,-102.042952999999997 37.803534999999997,-102.043032999999994 37.824145999999999,-102.043218999999993 37.86792899999999,-102.043714531331204 37.914003914798144,-102.043845000000005 37.926135000000002,-102.043844000000007 37.928101999999996,-102.044644000000005 38.045532,-102.044255000000007 38.113010999999993,-102.044450868202546 38.120049353793199,-102.044589000000002 38.125012999999996,-102.044415729020699 38.133607343100145,-102.044251000000003 38.141777999999995,-102.044296878804843 38.175558844899406,-102.044398 38.250014999999998,-102.044510721728557 38.262483349316234,-102.044567999999998 38.268819,-102.044612999999998 38.312323999999997,-102.044944 38.384419,-102.044441999999989 38.415801999999999,-102.044936000000007 38.41968,-102.045323999999979 38.453646999999997,-102.045262999999991 38.505395,-102.045261999999994 38.505531999999995,-102.045112000000003 38.523783999999999,-102.045222999999993 38.543796999999998,-102.045188999999979 38.558731999999992,-102.045210999999995 38.581608999999993,-102.045287999999999 38.615248999999999,-102.045074 38.669617000000002,-102.045102 38.674945999999998,-102.045159999999996 38.675221,-102.045126999999994 38.686725000000003,-102.045156000000006 38.688555,-102.045211999999992 38.697566999999999,-102.045375000000007 38.754338999999995,-102.045287000000002 38.755527999999998,-102.045371000000003 38.770063999999998,-102.045447999999993 38.783453000000002,-102.045333999999997 38.799463000000003,-102.045387999999988 38.813391999999993,-102.046570999999986 39.047038,-102.047133999999986 39.129700999999997,-102.047188612927897 39.133146793270186,-102.047250000000005 39.13702,-102.047851021058236 39.220289738242592,-102.048449000000005 39.30313799999999,-102.04895999999998 39.37371199999999,-102.049100972149461 39.394064428437375,-102.049166999999983 39.403596999999998,-102.049369999999996 39.418210000000002,-102.049368999999999 39.423333,-102.049678999999998 39.506183,-102.049672999999999 39.536690999999998,-102.049553999999986 39.538932000000003,-102.049763758475066 39.568170000775439,-102.04980599999999 39.574058,-102.049954 39.592331,-102.050258163748609 39.627242889069393,-102.050421999999983 39.646047999999993,-102.050099000000003 39.653812000000002,-102.05059399999999 39.67559399999999,-102.050898693231389 39.741794606053567,-102.051254 39.818992,-102.051317999999995 39.833311000000002,-102.051362999999995 39.843471,-102.051569 39.849805000000003,-102.051743999999999 40.003078000000002))" - } - } -] diff --git a/django/contrib/gis/tests/geoapp/fixtures/initial_data.json.gz b/django/contrib/gis/tests/geoapp/fixtures/initial_data.json.gz new file mode 100644 index 000000000000..c6950822cfb0 Binary files /dev/null and b/django/contrib/gis/tests/geoapp/fixtures/initial_data.json.gz differ diff --git a/django/contrib/gis/tests/geoapp/test_feeds.py b/django/contrib/gis/tests/geoapp/test_feeds.py index eb039145e467..ddd616eb1840 100644 --- a/django/contrib/gis/tests/geoapp/test_feeds.py +++ b/django/contrib/gis/tests/geoapp/test_feeds.py @@ -1,11 +1,22 @@ -import unittest from xml.dom import minidom +from django.conf import settings +from django.contrib.sites.models import Site +from django.test import TestCase -from django.test import Client from models import City -class GeoFeedTest(unittest.TestCase): - client = Client() + +class GeoFeedTest(TestCase): + + urls = 'django.contrib.gis.tests.geoapp.urls' + + def setUp(self): + Site(id=settings.SITE_ID, domain="example.com", name="example.com").save() + self.old_Site_meta_installed = Site._meta.installed + Site._meta.installed = True + + def tearDown(self): + Site._meta.installed = self.old_Site_meta_installed def assertChildNodes(self, elem, expected): "Taken from regressiontests/syndication/tests.py." @@ -16,9 +27,9 @@ def assertChildNodes(self, elem, expected): def test_geofeed_rss(self): "Tests geographic feeds using GeoRSS over RSSv2." # Uses `GEOSGeometry` in `item_geometry` - doc1 = minidom.parseString(self.client.get('/geoapp/feeds/rss1/').content) + doc1 = minidom.parseString(self.client.get('/feeds/rss1/').content) # Uses a 2-tuple in `item_geometry` - doc2 = minidom.parseString(self.client.get('/geoapp/feeds/rss2/').content) + doc2 = minidom.parseString(self.client.get('/feeds/rss2/').content) feed1, feed2 = doc1.firstChild, doc2.firstChild # Making sure the box got added to the second GeoRSS feed. @@ -41,8 +52,8 @@ def test_geofeed_rss(self): def test_geofeed_atom(self): "Testing geographic feeds using GeoRSS over Atom." - doc1 = minidom.parseString(self.client.get('/geoapp/feeds/atom1/').content) - doc2 = minidom.parseString(self.client.get('/geoapp/feeds/atom2/').content) + doc1 = minidom.parseString(self.client.get('/feeds/atom1/').content) + doc2 = minidom.parseString(self.client.get('/feeds/atom2/').content) feed1, feed2 = doc1.firstChild, doc2.firstChild # Making sure the box got added to the second GeoRSS feed. @@ -60,7 +71,7 @@ def test_geofeed_atom(self): def test_geofeed_w3c(self): "Testing geographic feeds using W3C Geo." - doc = minidom.parseString(self.client.get('/geoapp/feeds/w3cgeo1/').content) + doc = minidom.parseString(self.client.get('/feeds/w3cgeo1/').content) feed = doc.firstChild # Ensuring the geo namespace was added to the element. self.assertEqual(feed.getAttribute(u'xmlns:geo'), u'http://www.w3.org/2003/01/geo/wgs84_pos#') @@ -73,5 +84,5 @@ def test_geofeed_w3c(self): self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'geo:lat', 'geo:lon']) # Boxes and Polygons aren't allowed in W3C Geo feeds. - self.assertRaises(ValueError, self.client.get, '/geoapp/feeds/w3cgeo2/') # Box in - self.assertRaises(ValueError, self.client.get, '/geoapp/feeds/w3cgeo3/') # Polygons in + self.assertRaises(ValueError, self.client.get, '/feeds/w3cgeo2/') # Box in + self.assertRaises(ValueError, self.client.get, '/feeds/w3cgeo3/') # Polygons in diff --git a/django/contrib/gis/tests/geoapp/test_sitemaps.py b/django/contrib/gis/tests/geoapp/test_sitemaps.py index a83e044b8698..16e04337c687 100644 --- a/django/contrib/gis/tests/geoapp/test_sitemaps.py +++ b/django/contrib/gis/tests/geoapp/test_sitemaps.py @@ -1,11 +1,24 @@ -import unittest, zipfile, cStringIO +import cStringIO from xml.dom import minidom +import zipfile +from django.conf import settings +from django.contrib.sites.models import Site +from django.test import TestCase -from django.test import Client from models import City, Country -class GeoSitemapTest(unittest.TestCase): - client = Client() + +class GeoSitemapTest(TestCase): + + urls = 'django.contrib.gis.tests.geoapp.urls' + + def setUp(self): + Site(id=settings.SITE_ID, domain="example.com", name="example.com").save() + self.old_Site_meta_installed = Site._meta.installed + Site._meta.installed = True + + def tearDown(self): + Site._meta.installed = self.old_Site_meta_installed def assertChildNodes(self, elem, expected): "Taken from regressiontests/syndication/tests.py." @@ -16,7 +29,7 @@ def assertChildNodes(self, elem, expected): def test_geositemap_index(self): "Tests geographic sitemap index." # Getting the geo index. - doc = minidom.parseString(self.client.get('/geoapp/sitemap.xml').content) + doc = minidom.parseString(self.client.get('/sitemap.xml').content) index = doc.firstChild self.assertEqual(index.getAttribute(u'xmlns'), u'http://www.sitemaps.org/schemas/sitemap/0.9') self.assertEqual(3, len(index.getElementsByTagName('sitemap'))) @@ -24,13 +37,13 @@ def test_geositemap_index(self): def test_geositemap_kml(self): "Tests KML/KMZ geographic sitemaps." for kml_type in ('kml', 'kmz'): - doc = minidom.parseString(self.client.get('/geoapp/sitemaps/%s.xml' % kml_type).content) + doc = minidom.parseString(self.client.get('/sitemaps/%s.xml' % kml_type).content) # Ensuring the right sitemaps namespaces are present. urlset = doc.firstChild self.assertEqual(urlset.getAttribute(u'xmlns'), u'http://www.sitemaps.org/schemas/sitemap/0.9') self.assertEqual(urlset.getAttribute(u'xmlns:geo'), u'http://www.google.com/geo/schemas/sitemap/1.0') - + urls = urlset.getElementsByTagName('url') self.assertEqual(2, len(urls)) # Should only be 2 sitemaps. for url in urls: @@ -42,7 +55,7 @@ def test_geositemap_kml(self): # Getting the relative URL since we don't have a real site. kml_url = url.getElementsByTagName('loc')[0].childNodes[0].data.split('http://example.com')[1] - + if kml_type == 'kml': kml_doc = minidom.parseString(self.client.get(kml_url).content) elif kml_type == 'kmz': @@ -52,7 +65,7 @@ def test_geositemap_kml(self): self.assertEqual(1, len(zf.filelist)) self.assertEqual('doc.kml', zf.filelist[0].filename) kml_doc = minidom.parseString(zf.read('doc.kml')) - + # Ensuring the correct number of placemarks are in the KML doc. if 'city' in kml_url: model = City @@ -64,8 +77,8 @@ def test_geositemap_georss(self): "Tests GeoRSS geographic sitemaps." from feeds import feed_dict - doc = minidom.parseString(self.client.get('/geoapp/sitemaps/georss.xml').content) - + doc = minidom.parseString(self.client.get('/sitemaps/georss.xml').content) + # Ensuring the right sitemaps namespaces are present. urlset = doc.firstChild self.assertEqual(urlset.getAttribute(u'xmlns'), u'http://www.sitemaps.org/schemas/sitemap/0.9') diff --git a/django/contrib/gis/tests/geoapp/tests.py b/django/contrib/gis/tests/geoapp/tests.py index ab915896e597..a1f251ddda78 100644 --- a/django/contrib/gis/tests/geoapp/tests.py +++ b/django/contrib/gis/tests/geoapp/tests.py @@ -1,10 +1,11 @@ -import re, os, unittest +import re from django.db import connection from django.contrib.gis import gdal -from django.contrib.gis.geos import * +from django.contrib.gis.geos import fromstr, GEOSGeometry, \ + Point, LineString, LinearRing, Polygon, GeometryCollection from django.contrib.gis.measure import Distance from django.contrib.gis.tests.utils import \ - no_mysql, no_oracle, no_postgis, no_spatialite, \ + no_mysql, no_oracle, no_spatialite, \ mysql, oracle, postgis, spatialite from django.test import TestCase @@ -698,7 +699,7 @@ def test28_reverse(self): self.assertEqual(tuple(coords), t.reverse_geom.coords) if oracle: self.assertRaises(TypeError, State.objects.reverse_geom) - + @no_mysql @no_oracle @no_spatialite @@ -717,7 +718,7 @@ def test29_force_rhr(self): @no_mysql @no_oracle @no_spatialite - def test29_force_rhr(self): + def test30_geohash(self): "Testing GeoQuerySet.geohash()." if not connection.ops.geohash: return # Reference query: @@ -732,11 +733,3 @@ def test29_force_rhr(self): from test_feeds import GeoFeedTest from test_regress import GeoRegressionTests from test_sitemaps import GeoSitemapTest - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(GeoModelTest)) - s.addTest(unittest.makeSuite(GeoFeedTest)) - s.addTest(unittest.makeSuite(GeoSitemapTest)) - s.addTest(unittest.makeSuite(GeoRegressionTests)) - return s diff --git a/django/contrib/gis/tests/geogapp/models.py b/django/contrib/gis/tests/geogapp/models.py index 3198fbd36d71..3696ba2ff421 100644 --- a/django/contrib/gis/tests/geogapp/models.py +++ b/django/contrib/gis/tests/geogapp/models.py @@ -10,7 +10,7 @@ class Zipcode(models.Model): code = models.CharField(max_length=10) poly = models.PolygonField(geography=True) objects = models.GeoManager() - def __unicode__(self): return self.name + def __unicode__(self): return self.code class County(models.Model): name = models.CharField(max_length=25) diff --git a/django/contrib/gis/tests/geogapp/tests.py b/django/contrib/gis/tests/geogapp/tests.py index 7be519310317..cb69ce94e1f2 100644 --- a/django/contrib/gis/tests/geogapp/tests.py +++ b/django/contrib/gis/tests/geogapp/tests.py @@ -44,6 +44,10 @@ def test04_invalid_operators_functions(self): # `@` operator not available. self.assertRaises(ValueError, City.objects.filter(point__contained=z.poly).count) + # Regression test for #14060, `~=` was never really implemented for PostGIS. + htown = City.objects.get(name='Houston') + self.assertRaises(ValueError, City.objects.get, point__exact=htown.point) + def test05_geography_layermapping(self): "Testing LayerMapping support on models with geography fields." # There is a similar test in `layermap` that uses the same data set, @@ -72,3 +76,12 @@ def test05_geography_layermapping(self): self.assertEqual(num_poly, len(c.mpoly)) self.assertEqual(name, c.name) self.assertEqual(state, c.state) + + def test06_geography_area(self): + "Testing that Area calculations work on geography columns." + from django.contrib.gis.measure import A + # SELECT ST_Area(poly) FROM geogapp_zipcode WHERE code='77002'; + ref_area = 5439084.70637573 + tol = 5 + z = Zipcode.objects.area().get(code='77002') + self.assertAlmostEqual(z.area.sq_m, ref_area, tol) diff --git a/django/contrib/gis/tests/geometries.py b/django/contrib/gis/tests/geometries.py deleted file mode 100644 index 6689cd710323..000000000000 --- a/django/contrib/gis/tests/geometries.py +++ /dev/null @@ -1,180 +0,0 @@ -import re - -wkt_regex = re.compile(r'^(?P[A-Z]+) ?\(') - -class TestGeom: - "The Test Geometry class container." - def __init__(self, wkt, **kwargs): - self.wkt = wkt - - self.bad = kwargs.pop('bad', False) - - if not self.bad: - m = wkt_regex.match(wkt) - if not m: - raise Exception('Improper WKT: "%s"' % wkt) - self.geo_type = m.group('type') - - for key, value in kwargs.items(): - setattr(self, key, value) - -# For the old tests -swig_geoms = (TestGeom('POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))', ncoords=5), - TestGeom('POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (10 10, 10 90, 90 90, 90 10, 10 10) ))', ncoords=10), - ) - -# Testing WKT & HEX -hex_wkt = (TestGeom('POINT(0 1)', hex='01010000000000000000000000000000000000F03F'), - TestGeom('LINESTRING(0 1, 2 3, 4 5)', hex='0102000000030000000000000000000000000000000000F03F0000000000000040000000000000084000000000000010400000000000001440'), - TestGeom('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))', hex='010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000'), - TestGeom('MULTIPOINT(0 0, 10 0, 10 10, 0 10, 0 0)', hex='010400000005000000010100000000000000000000000000000000000000010100000000000000000024400000000000000000010100000000000000000024400000000000002440010100000000000000000000000000000000002440010100000000000000000000000000000000000000'), - TestGeom('MULTILINESTRING((0 0, 10 0, 10 10, 0 10),(20 20, 30 20))', hex='01050000000200000001020000000400000000000000000000000000000000000000000000000000244000000000000000000000000000002440000000000000244000000000000000000000000000002440010200000002000000000000000000344000000000000034400000000000003E400000000000003440'), - TestGeom('MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)),((20 20, 20 30, 30 30, 30 20, 20 20),(25 25, 25 26, 26 26, 26 25, 25 25)))', hex='010600000002000000010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000010300000002000000050000000000000000003440000000000000344000000000000034400000000000003E400000000000003E400000000000003E400000000000003E40000000000000344000000000000034400000000000003440050000000000000000003940000000000000394000000000000039400000000000003A400000000000003A400000000000003A400000000000003A40000000000000394000000000000039400000000000003940'), - TestGeom('GEOMETRYCOLLECTION(MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0)),((20 20, 20 30, 30 30, 30 20, 20 20),(25 25, 25 26, 26 26, 26 25, 25 25))),MULTILINESTRING((0 0, 10 0, 10 10, 0 10),(20 20, 30 20)),MULTIPOINT(0 0, 10 0, 10 10, 0 10, 0 0))', hex='010700000003000000010600000002000000010300000001000000050000000000000000000000000000000000000000000000000024400000000000000000000000000000244000000000000024400000000000000000000000000000244000000000000000000000000000000000010300000002000000050000000000000000003440000000000000344000000000000034400000000000003E400000000000003E400000000000003E400000000000003E40000000000000344000000000000034400000000000003440050000000000000000003940000000000000394000000000000039400000000000003A400000000000003A400000000000003A400000000000003A4000000000000039400000000000003940000000000000394001050000000200000001020000000400000000000000000000000000000000000000000000000000244000000000000000000000000000002440000000000000244000000000000000000000000000002440010200000002000000000000000000344000000000000034400000000000003E400000000000003440010400000005000000010100000000000000000000000000000000000000010100000000000000000024400000000000000000010100000000000000000024400000000000002440010100000000000000000000000000000000002440010100000000000000000000000000000000000000'), - ) - -# WKT, GML, KML output -wkt_out = (TestGeom('POINT (110 130)', ewkt='POINT (110.0000000000000000 130.0000000000000000)', kml='110.0,130.0,0', gml='110,130'), - TestGeom('LINESTRING (40 40,50 130,130 130)', ewkt='LINESTRING (40.0000000000000000 40.0000000000000000, 50.0000000000000000 130.0000000000000000, 130.0000000000000000 130.0000000000000000)', kml='40.0,40.0,0 50.0,130.0,0 130.0,130.0,0', gml='40,40 50,130 130,130'), - TestGeom('POLYGON ((150 150,410 150,280 20,20 20,150 150),(170 120,330 120,260 50,100 50,170 120))', ewkt='POLYGON ((150.0000000000000000 150.0000000000000000, 410.0000000000000000 150.0000000000000000, 280.0000000000000000 20.0000000000000000, 20.0000000000000000 20.0000000000000000, 150.0000000000000000 150.0000000000000000), (170.0000000000000000 120.0000000000000000, 330.0000000000000000 120.0000000000000000, 260.0000000000000000 50.0000000000000000, 100.0000000000000000 50.0000000000000000, 170.0000000000000000 120.0000000000000000))', kml='150.0,150.0,0 410.0,150.0,0 280.0,20.0,0 20.0,20.0,0 150.0,150.0,0170.0,120.0,0 330.0,120.0,0 260.0,50.0,0 100.0,50.0,0 170.0,120.0,0', gml='150,150 410,150 280,20 20,20 150,150170,120 330,120 260,50 100,50 170,120'), - TestGeom('MULTIPOINT (10 80,110 170,110 120)', ewkt='MULTIPOINT (10.0000000000000000 80.0000000000000000, 110.0000000000000000 170.0000000000000000, 110.0000000000000000 120.0000000000000000)', kml='10.0,80.0,0110.0,170.0,0110.0,120.0,0', gml='10,80110,170110,120'), - TestGeom('MULTILINESTRING ((110 100,40 30,180 30),(170 30,110 90,50 30))', ewkt='MULTILINESTRING ((110.0000000000000000 100.0000000000000000, 40.0000000000000000 30.0000000000000000, 180.0000000000000000 30.0000000000000000), (170.0000000000000000 30.0000000000000000, 110.0000000000000000 90.0000000000000000, 50.0000000000000000 30.0000000000000000))', kml='110.0,100.0,0 40.0,30.0,0 180.0,30.0,0170.0,30.0,0 110.0,90.0,0 50.0,30.0,0', gml='110,100 40,30 180,30170,30 110,90 50,30'), - TestGeom('MULTIPOLYGON (((110 110,70 200,150 200,110 110),(110 110,100 180,120 180,110 110)),((110 110,150 20,70 20,110 110),(110 110,120 40,100 40,110 110)))', ewkt='MULTIPOLYGON (((110.0000000000000000 110.0000000000000000, 70.0000000000000000 200.0000000000000000, 150.0000000000000000 200.0000000000000000, 110.0000000000000000 110.0000000000000000), (110.0000000000000000 110.0000000000000000, 100.0000000000000000 180.0000000000000000, 120.0000000000000000 180.0000000000000000, 110.0000000000000000 110.0000000000000000)), ((110.0000000000000000 110.0000000000000000, 150.0000000000000000 20.0000000000000000, 70.0000000000000000 20.0000000000000000, 110.0000000000000000 110.0000000000000000), (110.0000000000000000 110.0000000000000000, 120.0000000000000000 40.0000000000000000, 100.0000000000000000 40.0000000000000000, 110.0000000000000000 110.0000000000000000)))', kml='110.0,110.0,0 70.0,200.0,0 150.0,200.0,0 110.0,110.0,0110.0,110.0,0 100.0,180.0,0 120.0,180.0,0 110.0,110.0,0110.0,110.0,0 150.0,20.0,0 70.0,20.0,0 110.0,110.0,0110.0,110.0,0 120.0,40.0,0 100.0,40.0,0 110.0,110.0,0', gml='110,110 70,200 150,200 110,110110,110 100,180 120,180 110,110110,110 150,20 70,20 110,110110,110 120,40 100,40 110,110'), - TestGeom('GEOMETRYCOLLECTION (POINT (110 260),LINESTRING (110 0,110 60))', ewkt='GEOMETRYCOLLECTION (POINT (110.0000000000000000 260.0000000000000000), LINESTRING (110.0000000000000000 0.0000000000000000, 110.0000000000000000 60.0000000000000000))', kml='110.0,260.0,0110.0,0.0,0 110.0,60.0,0', gml='110,260110,0 110,60'), - ) - -# Errors -errors = (TestGeom('GEOMETR##!@#%#............a32515', bad=True, hex=False), - TestGeom('Foo.Bar', bad=True, hex=False), - TestGeom('POINT (5, 23)', bad=True, hex=False), - TestGeom('AAABBBDDDAAD##@#1113511111-098111111111111111533333333333333', bad=True, hex=True), - TestGeom('FFFFFFFFFFFFFFFFF1355555555555555555565111', bad=True, hex=True), - TestGeom('', bad=True, hex=False), - ) - -# Polygons -polygons = (TestGeom('POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (10 10, 10 90, 90 90, 90 10, 10 10))', - n_i=1, ext_ring_cs=((0, 0), (0, 100), (100, 100), (100, 0), (0, 0)), n_p=10, area=3600.0, centroid=(50., 50.), - ), - TestGeom('POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (10 10, 10 20, 20 20, 20 10, 10 10), (80 80, 80 90, 90 90, 90 80, 80 80))', - n_i=2, ext_ring_cs=((0, 0), (0, 100), (100, 100), (100, 0), (0, 0)), n_p=15, area=9800.0, centroid=(50., 50.), - ), - TestGeom('POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))', - n_i=0, ext_ring_cs=((0, 0), (0, 100), (100, 100), (100, 0), (0, 0)), n_p=5, area=10000.0, centroid=(50., 50.), - ), - TestGeom('POLYGON ((-95.3848703124799471 29.7056021479768511, -95.3851905195191847 29.7046588196500281, -95.3859356966379011 29.7025053545605502, -95.3860723000647539 29.7020963367038391, -95.3871517697222089 29.6989779021280995, -95.3865578518265522 29.6990856888057202, -95.3862634205175226 29.6999471753441782, -95.3861991779541967 29.6999591988978615, -95.3856773799358137 29.6998323107113578, -95.3856209915427229 29.6998005235473741, -95.3855833545501639 29.6996619391729801, -95.3855776331865002 29.6996232659570047, -95.3850162731712885 29.6997236706530536, -95.3831047357410284 29.7000847603095082, -95.3829800724914776 29.7000676365023502, -95.3828084594470909 29.6999969684031200, -95.3828131504821499 29.6999090511531065, -95.3828022942979601 29.6998152117366025, -95.3827893930918833 29.6997790953076759, -95.3825174668099862 29.6998267772748825, -95.3823521544804862 29.7000451723151606, -95.3820491918785223 29.6999682034582335, -95.3817932841505893 29.6999640407204772, -95.3815438924600443 29.7005983712500630, -95.3807812390843424 29.7007538492921590, -95.3778578936435935 29.7012966201172048, -95.3770817300034679 29.7010555145969093, -95.3772763716395957 29.7004995005932031, -95.3769891024414420 29.7005797730360186, -95.3759855007185990 29.7007754783987821, -95.3759516423090474 29.7007305400669388, -95.3765252155960042 29.6989549173240874, -95.3766842746727832 29.6985134987163164, -95.3768510987262914 29.6980530300744938, -95.3769198676258014 29.6977137204527573, -95.3769616670751930 29.6973351617272172, -95.3770309229297766 29.6969821084304186, -95.3772352596880637 29.6959751305871613, -95.3776232419333354 29.6945439060847463, -95.3776849628727064 29.6943364710766069, -95.3779699491714723 29.6926548349458947, -95.3781945479573494 29.6920088336742545, -95.3785807118394189 29.6908279316076005, -95.3787441368896651 29.6908846275832197, -95.3787903214163890 29.6907152912461640, -95.3791765069353659 29.6893335376821526, -95.3794935959513026 29.6884781789101595, -95.3796592071232112 29.6880066681407619, -95.3799788182090111 29.6873687353035081, -95.3801545516183893 29.6868782380716993, -95.3801258908302145 29.6867756621337762, -95.3801104284899566 29.6867229678809572, -95.3803803523746154 29.6863753372986459, -95.3821028558287622 29.6837392961470421, -95.3827289584682205 29.6828097375216160, -95.3827494698109035 29.6790739156259278, -95.3826022014838486 29.6776502228345507, -95.3825047356438063 29.6765773006280753, -95.3823473035336917 29.6750405250369127, -95.3824540163482055 29.6750076408228587, -95.3838984230304305 29.6745679207378679, -95.3916547074937426 29.6722459226508377, -95.3926154662749468 29.6719609085105489, -95.3967246645118081 29.6707316485589736, -95.3974588054406780 29.6705065336410989, -95.3978523748756828 29.6703795547846845, -95.3988598162279970 29.6700874981900853, -95.3995628600665952 29.6698505300412414, -95.4134721665944170 29.6656841279906232, -95.4143262068232616 29.6654291174019278, -95.4159685142480214 29.6649750989232288, -95.4180067396277565 29.6643253024318021, -95.4185886692196590 29.6641482768691063, -95.4234155309609662 29.6626925393704788, -95.4287785503196346 29.6611023620959706, -95.4310287312749352 29.6604222580752648, -95.4320295629628959 29.6603361318136720, -95.4332899683975739 29.6600560661713608, -95.4342675748811047 29.6598454934599900, -95.4343110414310871 29.6598411486215490, -95.4345576779282538 29.6598147020668499, -95.4348823041721630 29.6597875803673112, -95.4352827715209457 29.6597762346946681, -95.4355290431309982 29.6597827926562374, -95.4359197997999331 29.6598014511782715, -95.4361907884752156 29.6598444333523368, -95.4364608955807228 29.6598901433108217, -95.4367250147512323 29.6599494499910712, -95.4364898759758091 29.6601880616540186, -95.4354501111810691 29.6616378572201107, -95.4381459623171224 29.6631265631655126, -95.4367852490863129 29.6642266600024023, -95.4370040894557263 29.6643425389568769, -95.4367078350812648 29.6645492592343238, -95.4366081749871285 29.6646291473027297, -95.4358539359938192 29.6652308742342932, -95.4350327668927889 29.6658995989314462, -95.4350580905272921 29.6678812477895271, -95.4349710541447536 29.6680054925936965, -95.4349500440473548 29.6671410080890006, -95.4341492724148850 29.6678790545191688, -95.4340248868274728 29.6680353198492135, -95.4333227845797438 29.6689245624945990, -95.4331325652123326 29.6691616138940901, -95.4321314741096955 29.6704473333237253, -95.4320435792664341 29.6702578985411982, -95.4320147929883547 29.6701800936425109, -95.4319764538662980 29.6683246590817085, -95.4317490976340679 29.6684974372577166, -95.4305958185342718 29.6694049049170374, -95.4296600735653016 29.6701723430938493, -95.4284928989940937 29.6710931793380972, -95.4274630532378580 29.6719378813640091, -95.4273056811974811 29.6720684984625791, -95.4260554084574864 29.6730668861566969, -95.4253558063699643 29.6736342467365724, -95.4249278826026028 29.6739557343648919, -95.4248648873821423 29.6745400910786152, -95.4260016131471929 29.6750987014005858, -95.4258567183010911 29.6753452063069929, -95.4260238081486847 29.6754322077221353, -95.4258707374502393 29.6756647377294307, -95.4257951755816691 29.6756407098663360, -95.4257701599566985 29.6761077719536068, -95.4257726684792260 29.6761711204603955, -95.4257980187195614 29.6770219651929423, -95.4252712669032519 29.6770161558853758, -95.4249234392992065 29.6770068683962300, -95.4249574272905789 29.6779707498635759, -95.4244725881033702 29.6779825646764159, -95.4222269476429545 29.6780711474441716, -95.4223032371999267 29.6796029391538809, -95.4239133706588945 29.6795331493690355, -95.4224579084327331 29.6813706893847780, -95.4224290108823965 29.6821953228763924, -95.4230916478977349 29.6822130268724109, -95.4222928279595521 29.6832041816675343, -95.4228763710016352 29.6832087677714505, -95.4223401691637179 29.6838987872753748, -95.4211655906087088 29.6838784024852984, -95.4201984153205558 29.6851319258758082, -95.4206156387716362 29.6851623398125319, -95.4213438084897660 29.6851763011334739, -95.4212071118618752 29.6853679931624974, -95.4202651399651245 29.6865313962980508, -95.4172061157659783 29.6865816431043932, -95.4182217951255183 29.6872251197301544, -95.4178664826439160 29.6876750901471631, -95.4180678442928780 29.6877960336377207, -95.4188763472917572 29.6882826379510938, -95.4185374500596311 29.6887137897831934, -95.4182121713132290 29.6885097429738813, -95.4179857231741551 29.6888118367840086, -95.4183106010563620 29.6890048676118212, -95.4179489865331334 29.6894546700979056, -95.4175581746284820 29.6892323606815438, -95.4173439957341571 29.6894990139807007, -95.4177411199311081 29.6897435034738422, -95.4175789200209721 29.6899207529979208, -95.4170598559864800 29.6896042165807508, -95.4166733682539814 29.6900891174451367, -95.4165941362704331 29.6900347214235047, -95.4163537218065301 29.6903529467753238, -95.4126843270708775 29.6881086357212780, -95.4126604121378392 29.6880942378803496, -95.4126672298953338 29.6885951670109982, -95.4126680884821923 29.6887052446594275, -95.4158080137241882 29.6906382377959339, -95.4152061403821961 29.6910871045531586, -95.4155842583188161 29.6917382915894308, -95.4157426793520358 29.6920726941677096, -95.4154520563662203 29.6922052332446427, -95.4151389936167078 29.6923261661269571, -95.4148649784384872 29.6924343866430256, -95.4144051352401590 29.6925623927348106, -95.4146792019416665 29.6926770338507744, -95.4148824479948985 29.6928117893696388, -95.4149851734360226 29.6929823719519774, -95.4140436551925291 29.6929626643100946, -95.4140465993023241 29.6926545917254892, -95.4137269186733334 29.6927395764256090, -95.4137372859685513 29.6935432485666624, -95.4135702836218655 29.6933186678088283, -95.4133925235973237 29.6930415229852152, -95.4133017035615580 29.6928685062036166, -95.4129588921634593 29.6929391128977862, -95.4125107395559695 29.6930481664661485, -95.4102647423187307 29.6935850183258019, -95.4081931340840157 29.6940907430947760, -95.4078783596459772 29.6941703429951609, -95.4049213975000043 29.6948723732981961, -95.4045944244127071 29.6949626434239207, -95.4045865139788134 29.6954109019001358, -95.4045953345484037 29.6956972800496963, -95.4038879332535146 29.6958296089365490, -95.4040366394459340 29.6964389004769842, -95.4032774779020798 29.6965643341263892, -95.4026066501239853 29.6966646227683881, -95.4024991226393837 29.6961389766619703, -95.4011781398631911 29.6963566063186377, -95.4011524097636112 29.6962596176762190, -95.4018184046368276 29.6961399466727336, -95.4016995838361908 29.6956442609415099, -95.4007100753964608 29.6958900524002978, -95.4008032469935188 29.6962639900781404, -95.3995660267125487 29.6965636449370329, -95.3996140564775601 29.6967877962763644, -95.3996364430014410 29.6968901984825280, -95.3984003269631842 29.6968679634805746, -95.3981442026887265 29.6983660679730335, -95.3980178461957706 29.6990890276252415, -95.3977097967130163 29.7008526152273049, -95.3962347157626027 29.7009697553607630, -95.3951949050136250 29.7004740386619019, -95.3957564950617183 29.6990281830553187, -95.3965927101519924 29.6968771129030706, -95.3957496517238184 29.6970800358387095, -95.3957720559467361 29.6972264611230727, -95.3957391586571788 29.6973548894558732, -95.3956286413405365 29.6974949857280883, -95.3955111053256957 29.6975661086270186, -95.3953215342724121 29.6976022763384790, -95.3951795558443365 29.6975846977491038, -95.3950369632041060 29.6975175779330200, -95.3949401089966500 29.6974269267953304, -95.3948740281415581 29.6972903308506346, -95.3946650813866910 29.6973397326847923, -95.3947654059391112 29.6974882560192022, -95.3949627316619768 29.6980355864961858, -95.3933200807862249 29.6984590863712796, -95.3932606497523494 29.6984464798710839, -95.3932983699113350 29.6983154306484352, -95.3933058014696655 29.6982165816983610, -95.3932946347785133 29.6981089778195759, -95.3931780601756287 29.6977068906794841, -95.3929928222970602 29.6977541771878180, -95.3930873169846478 29.6980676264932946, -95.3932743746374570 29.6981249406449663, -95.3929512584706316 29.6989526513922222, -95.3919850280655197 29.7014358632108646, -95.3918950918929056 29.7014169320765724, -95.3916928317890296 29.7019232352846423, -95.3915424614970959 29.7022988712928289, -95.3901530441668939 29.7058519502930061, -95.3899656322116698 29.7059156823562418, -95.3897628748670883 29.7059900058266777, -95.3896062677805787 29.7060738276384946, -95.3893941800512266 29.7061891695242046, -95.3892150365492455 29.7062641292949436, -95.3890502563035199 29.7063339729630940, -95.3888717930715586 29.7063896908080736, -95.3886925428988945 29.7064453871994978, -95.3885376849411983 29.7064797304524149, -95.3883284158984139 29.7065153575050189, -95.3881046767627794 29.7065368368267357, -95.3878809284696132 29.7065363048447537, -95.3876046356120924 29.7065288525102424, -95.3873060894974714 29.7064822806001452, -95.3869851943158409 29.7063993367575350, -95.3865967896568065 29.7062870572919202, -95.3861785624983156 29.7061492099008184, -95.3857375009733488 29.7059887337478798, -95.3854573290902152 29.7058683664514618, -95.3848703124799471 29.7056021479768511))', - n_i=0, ext_ring_cs=False, n_p=264, area=0.00129917360654, centroid=(-95.403569179437341, 29.681772571690402), - ), - ) - -# MultiPolygons -multipolygons = (TestGeom('MULTIPOLYGON (((100 20, 180 20, 180 100, 100 100, 100 20)), ((20 100, 100 100, 100 180, 20 180, 20 100)), ((100 180, 180 180, 180 260, 100 260, 100 180)), ((180 100, 260 100, 260 180, 180 180, 180 100)))', valid=True, num_geom=4, n_p=20), - TestGeom('MULTIPOLYGON (((60 300, 320 220, 260 60, 60 100, 60 300)), ((60 300, 320 220, 260 60, 60 100, 60 300)))', valid=False), - TestGeom('MULTIPOLYGON (((180 60, 240 160, 300 60, 180 60)), ((80 80, 180 60, 160 140, 240 160, 360 140, 300 60, 420 100, 320 280, 120 260, 80 80)))', valid=True, num_geom=2, n_p=14), - ) - -# Points -points = (TestGeom('POINT (5 23)', x=5.0, y=23.0, centroid=(5.0, 23.0)), - TestGeom('POINT (-95.338492 29.723893)', x=-95.338492, y=29.723893, centroid=(-95.338492, 29.723893)), - TestGeom('POINT(1.234 5.678)', x=1.234, y=5.678, centroid=(1.234, 5.678)), - TestGeom('POINT(4.321 8.765)', x=4.321, y=8.765, centroid=(4.321, 8.765)), - TestGeom('POINT(10 10)', x=10, y=10, centroid=(10., 10.)), - TestGeom('POINT (5 23 8)', x=5.0, y=23.0, z=8.0, centroid=(5.0, 23.0)), - ) - -# MultiPoints -multipoints = (TestGeom('MULTIPOINT(10 10, 20 20 )', n_p=2, points=((10., 10.), (20., 20.)), centroid=(15., 15.)), - TestGeom('MULTIPOINT(10 10, 20 20, 10 20, 20 10)', - n_p=4, points=((10., 10.), (20., 20.), (10., 20.), (20., 10.)), - centroid=(15., 15.)), - ) - -# LineStrings -linestrings = (TestGeom('LINESTRING (60 180, 120 100, 180 180)', n_p=3, centroid=(120, 140), tup=((60, 180), (120, 100), (180, 180))), - TestGeom('LINESTRING (0 0, 5 5, 10 5, 10 10)', n_p=4, centroid=(6.1611652351681556, 4.6966991411008934), tup=((0, 0), (5, 5), (10, 5), (10, 10)),), - ) - -# Linear Rings -linearrings = (TestGeom('LINEARRING (649899.3065171393100172 4176512.3807915160432458, 649902.7294133581453934 4176512.7834989596158266, 649906.5550170192727819 4176514.3942507002502680, 649910.5820134161040187 4176516.0050024418160319, 649914.4076170771149918 4176518.0184616246260703, 649917.2264131171396002 4176519.4278986593708396, 649920.0452871860470623 4176521.6427505780011415, 649922.0587463703704998 4176522.8507948759943247, 649924.2735982896992937 4176524.4616246484220028, 649926.2870574744883925 4176525.4683542405255139, 649927.8978092158213258 4176526.8777912775985897, 649929.3072462501004338 4176528.0858355751261115, 649930.1126611357321963 4176529.4952726080082357, 649927.4951798024121672 4176506.9444361114874482, 649899.3065171393100172 4176512.3807915160432458)', n_p=15), - ) - -# MultiLineStrings -multilinestrings = (TestGeom('MULTILINESTRING ((0 0, 0 100), (100 0, 100 100))', n_p=4, centroid=(50, 50), tup=(((0, 0), (0, 100)), ((100, 0), (100, 100)))), - TestGeom('MULTILINESTRING ((20 20, 60 60), (20 -20, 60 -60), (-20 -20, -60 -60), (-20 20, -60 60), (-80 0, 0 80, 80 0, 0 -80, -80 0), (-40 20, -40 -20), (-20 40, 20 40), (40 20, 40 -20), (20 -40, -20 -40))', - n_p=21, centroid=(0, 0), tup=(((20., 20.), (60., 60.)), ((20., -20.), (60., -60.)), ((-20., -20.), (-60., -60.)), ((-20., 20.), (-60., 60.)), ((-80., 0.), (0., 80.), (80., 0.), (0., -80.), (-80., 0.)), ((-40., 20.), (-40., -20.)), ((-20., 40.), (20., 40.)), ((40., 20.), (40., -20.)), ((20., -40.), (-20., -40.)))) - ) - -# ==================================================== -# Topology Operations - -topology_geoms = ( (TestGeom('POLYGON ((-5.0 0.0, -5.0 10.0, 5.0 10.0, 5.0 0.0, -5.0 0.0))'), - TestGeom('POLYGON ((0.0 -5.0, 0.0 5.0, 10.0 5.0, 10.0 -5.0, 0.0 -5.0))') - ), - (TestGeom('POLYGON ((2 0, 18 0, 18 15, 2 15, 2 0))'), - TestGeom('POLYGON ((10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))'), - ), - ) - -intersect_geoms = ( TestGeom('POLYGON ((5 5,5 0,0 0,0 5,5 5))'), - TestGeom('POLYGON ((10 1, 9 3, 7 4, 5 6, 4 8, 4 10, 5 12, 7 13, 9 12, 10 10, 11 12, 13 13, 15 12, 16 10, 16 8, 15 6, 13 4, 11 3, 10 1))'), - ) - -union_geoms = ( TestGeom('POLYGON ((-5 0,-5 10,5 10,5 5,10 5,10 -5,0 -5,0 0,-5 0))'), - TestGeom('POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0))'), - ) - -diff_geoms = ( TestGeom('POLYGON ((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0))'), - TestGeom('POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))'), - ) - -sdiff_geoms = ( TestGeom('MULTIPOLYGON (((-5 0,-5 10,5 10,5 5,0 5,0 0,-5 0)),((0 0,5 0,5 5,10 5,10 -5,0 -5,0 0)))'), - TestGeom('POLYGON ((2 0, 2 15, 18 15, 18 0, 2 0), (10 1, 11 3, 13 4, 15 6, 16 8, 16 10, 15 12, 13 13, 11 12, 10 10, 9 12, 7 13, 5 12, 4 10, 4 8, 5 6, 7 4, 9 3, 10 1))'), - ) - -relate_geoms = ( (TestGeom('MULTIPOINT(80 70, 20 20, 200 170, 140 120)'), - TestGeom('MULTIPOINT(80 170, 140 120, 200 80, 80 70)'), - '0F0FFF0F2', True,), - (TestGeom('POINT(20 20)'), TestGeom('POINT(40 60)'), - 'FF0FFF0F2', True,), - (TestGeom('POINT(110 110)'), TestGeom('LINESTRING(200 200, 110 110, 200 20, 20 20, 110 110, 20 200, 200 200)'), - '0FFFFF1F2', True,), - (TestGeom('MULTILINESTRING((20 20, 90 20, 170 20), (90 20, 90 80, 90 140))'), - TestGeom('MULTILINESTRING((90 20, 170 100, 170 140), (130 140, 130 60, 90 20, 20 90, 90 20))'), - 'FF10F0102', True,), - ) - -buffer_geoms = ( (TestGeom('POINT(0 0)'), - TestGeom('POLYGON ((5 0,4.903926402016153 -0.97545161008064,4.619397662556435 -1.913417161825447,4.157348061512728 -2.777851165098009,3.53553390593274 -3.535533905932735,2.777851165098015 -4.157348061512724,1.913417161825454 -4.619397662556431,0.975451610080648 -4.903926402016151,0.000000000000008 -5.0,-0.975451610080632 -4.903926402016154,-1.913417161825439 -4.619397662556437,-2.777851165098002 -4.157348061512732,-3.53553390593273 -3.535533905932746,-4.157348061512719 -2.777851165098022,-4.619397662556429 -1.913417161825462,-4.903926402016149 -0.975451610080656,-5.0 -0.000000000000016,-4.903926402016156 0.975451610080624,-4.619397662556441 1.913417161825432,-4.157348061512737 2.777851165097995,-3.535533905932752 3.535533905932723,-2.777851165098029 4.157348061512714,-1.913417161825468 4.619397662556426,-0.975451610080661 4.903926402016149,-0.000000000000019 5.0,0.975451610080624 4.903926402016156,1.913417161825434 4.61939766255644,2.777851165097998 4.157348061512735,3.535533905932727 3.535533905932748,4.157348061512719 2.777851165098022,4.619397662556429 1.91341716182546,4.90392640201615 0.975451610080652,5 0))'), - 5.0, 8), - (TestGeom('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))'), - TestGeom('POLYGON ((-2 0,-2 10,-1.961570560806461 10.390180644032258,-1.847759065022573 10.765366864730179,-1.662939224605091 11.111140466039204,-1.414213562373095 11.414213562373096,-1.111140466039204 11.662939224605092,-0.765366864730179 11.847759065022574,-0.390180644032256 11.961570560806461,0 12,10 12,10.390180644032256 11.961570560806461,10.765366864730179 11.847759065022574,11.111140466039204 11.66293922460509,11.414213562373096 11.414213562373096,11.66293922460509 11.111140466039204,11.847759065022574 10.765366864730179,11.961570560806461 10.390180644032256,12 10,12 0,11.961570560806461 -0.390180644032256,11.847759065022574 -0.76536686473018,11.66293922460509 -1.111140466039204,11.414213562373096 -1.414213562373095,11.111140466039204 -1.66293922460509,10.765366864730179 -1.847759065022573,10.390180644032256 -1.961570560806461,10 -2,0.0 -2.0,-0.390180644032255 -1.961570560806461,-0.765366864730177 -1.847759065022575,-1.1111404660392 -1.662939224605093,-1.41421356237309 -1.4142135623731,-1.662939224605086 -1.111140466039211,-1.84775906502257 -0.765366864730189,-1.961570560806459 -0.390180644032268,-2 0))'), - 2.0, 8), - ) - -json_geoms = (TestGeom('POINT(100 0)', json='{ "type": "Point", "coordinates": [ 100.000000, 0.000000 ] }'), - TestGeom('POLYGON((0 0, -10 0, -10 -10, 0 -10, 0 0))', json='{ "type": "Polygon", "coordinates": [ [ [ 0.000000, 0.000000 ], [ -10.000000, 0.000000 ], [ -10.000000, -10.000000 ], [ 0.000000, -10.000000 ], [ 0.000000, 0.000000 ] ] ] }'), - TestGeom('MULTIPOLYGON(((102 2, 103 2, 103 3, 102 3, 102 2)), ((100.0 0.0, 101.0 0.0, 101.0 1.0, 100.0 1.0, 100.0 0.0), (100.2 0.2, 100.8 0.2, 100.8 0.8, 100.2 0.8, 100.2 0.2)))', json='{ "type": "MultiPolygon", "coordinates": [ [ [ [ 102.000000, 2.000000 ], [ 103.000000, 2.000000 ], [ 103.000000, 3.000000 ], [ 102.000000, 3.000000 ], [ 102.000000, 2.000000 ] ] ], [ [ [ 100.000000, 0.000000 ], [ 101.000000, 0.000000 ], [ 101.000000, 1.000000 ], [ 100.000000, 1.000000 ], [ 100.000000, 0.000000 ] ], [ [ 100.200000, 0.200000 ], [ 100.800000, 0.200000 ], [ 100.800000, 0.800000 ], [ 100.200000, 0.800000 ], [ 100.200000, 0.200000 ] ] ] ] }'), - TestGeom('GEOMETRYCOLLECTION(POINT(100 0),LINESTRING(101.0 0.0, 102.0 1.0))', - json='{ "type": "GeometryCollection", "geometries": [ { "type": "Point", "coordinates": [ 100.000000, 0.000000 ] }, { "type": "LineString", "coordinates": [ [ 101.000000, 0.000000 ], [ 102.000000, 1.000000 ] ] } ] }', - ), - TestGeom('MULTILINESTRING((100.0 0.0, 101.0 1.0),(102.0 2.0, 103.0 3.0))', - json=""" - -{ "type": "MultiLineString", - "coordinates": [ - [ [100.0, 0.0], [101.0, 1.0] ], - [ [102.0, 2.0], [103.0, 3.0] ] - ] - } - -""", - not_equal=True, - ), - ) - -# For testing HEX(EWKB). -ogc_hex = '01010000000000000000000000000000000000F03F' -# `SELECT ST_AsHEXEWKB(ST_GeomFromText('POINT(0 1)', 4326));` -hexewkb_2d = '0101000020E61000000000000000000000000000000000F03F' -# `SELECT ST_AsHEXEWKB(ST_GeomFromEWKT('SRID=4326;POINT(0 1 2)'));` -hexewkb_3d = '01010000A0E61000000000000000000000000000000000F03F0000000000000040' diff --git a/django/contrib/gis/tests/layermap/models.py b/django/contrib/gis/tests/layermap/models.py index 3a34d16f3fe9..51213eb0b816 100644 --- a/django/contrib/gis/tests/layermap/models.py +++ b/django/contrib/gis/tests/layermap/models.py @@ -43,6 +43,9 @@ class ICity1(CityBase): class ICity2(ICity1): dt_time = models.DateTimeField(auto_now=True) +class Invalid(models.Model): + point = models.PointField() + # Mapping dictionaries for the models above. co_mapping = {'name' : 'Name', 'state' : {'name' : 'State'}, # ForeignKey's use another mapping dictionary for the _related_ Model (State in this case). diff --git a/django/contrib/gis/tests/layermap/tests.py b/django/contrib/gis/tests/layermap/tests.py index f8239cb46d5b..1ffd09117b89 100644 --- a/django/contrib/gis/tests/layermap/tests.py +++ b/django/contrib/gis/tests/layermap/tests.py @@ -4,16 +4,19 @@ from django.utils.copycompat import copy -from django.contrib.gis.gdal import DataSource +from django.contrib.gis.gdal import DataSource, OGRException from django.contrib.gis.tests.utils import mysql from django.contrib.gis.utils.layermapping import LayerMapping, LayerMapError, InvalidDecimal, MissingForeignKey -from models import City, County, CountyFeat, Interstate, ICity1, ICity2, State, city_mapping, co_mapping, cofeat_mapping, inter_mapping +from models import \ + City, County, CountyFeat, Interstate, ICity1, ICity2, Invalid, State, \ + city_mapping, co_mapping, cofeat_mapping, inter_mapping -shp_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'data')) +shp_path = os.path.realpath(os.path.join(os.path.dirname(__file__), os.pardir, 'data')) city_shp = os.path.join(shp_path, 'cities', 'cities.shp') co_shp = os.path.join(shp_path, 'counties', 'counties.shp') inter_shp = os.path.join(shp_path, 'interstates', 'interstates.shp') +invalid_shp = os.path.join(shp_path, 'invalid', 'emptypoints.shp') # Dictionaries to hold what's expected in the county shapefile. NAMES = ['Bexar', 'Galveston', 'Harris', 'Honolulu', 'Pueblo'] @@ -265,8 +268,11 @@ def test06_model_inheritance(self): self.assertEqual(6, ICity1.objects.count()) self.assertEqual(3, ICity2.objects.count()) - -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(LayerMapTest)) - return s + + def test07_invalid_layer(self): + "Tests LayerMapping on invalid geometries. See #15378." + invalid_mapping = {'point': 'POINT'} + lm = LayerMapping(Invalid, invalid_shp, invalid_mapping, + source_srs=4326) + lm.save(silent=True) + diff --git a/django/contrib/gis/tests/relatedapp/fixtures/initial_data.json.gz b/django/contrib/gis/tests/relatedapp/fixtures/initial_data.json.gz new file mode 100644 index 000000000000..68bf54c1b0d1 Binary files /dev/null and b/django/contrib/gis/tests/relatedapp/fixtures/initial_data.json.gz differ diff --git a/django/contrib/gis/tests/relatedapp/models.py b/django/contrib/gis/tests/relatedapp/models.py index 726f9826c0a2..2e9a62b61f26 100644 --- a/django/contrib/gis/tests/relatedapp/models.py +++ b/django/contrib/gis/tests/relatedapp/models.py @@ -38,6 +38,11 @@ class Author(models.Model): name = models.CharField(max_length=100) objects = models.GeoManager() +class Article(models.Model): + title = models.CharField(max_length=100) + author = models.ForeignKey(Author, unique=True) + objects = models.GeoManager() + class Book(models.Model): title = models.CharField(max_length=100) author = models.ForeignKey(Author, related_name='books', null=True) diff --git a/django/contrib/gis/tests/relatedapp/tests.py b/django/contrib/gis/tests/relatedapp/tests.py index 184b65b9c72a..c8aeb28d2a3f 100644 --- a/django/contrib/gis/tests/relatedapp/tests.py +++ b/django/contrib/gis/tests/relatedapp/tests.py @@ -1,23 +1,13 @@ -import os, unittest -from django.contrib.gis.geos import * +from django.test import TestCase + +from django.contrib.gis.geos import GEOSGeometry, Point, MultiPoint from django.contrib.gis.db.models import Collect, Count, Extent, F, Union from django.contrib.gis.geometry.backend import Geometry -from django.contrib.gis.tests.utils import mysql, oracle, postgis, spatialite, no_mysql, no_oracle, no_spatialite -from django.conf import settings -from models import City, Location, DirectoryEntry, Parcel, Book, Author - -cities = (('Aurora', 'TX', -97.516111, 33.058333), - ('Roswell', 'NM', -104.528056, 33.387222), - ('Kecksburg', 'PA', -79.460734, 40.18476), - ) +from django.contrib.gis.tests.utils import mysql, oracle, no_mysql, no_oracle, no_spatialite -class RelatedGeoModelTest(unittest.TestCase): +from models import City, Location, DirectoryEntry, Parcel, Book, Author, Article - def test01_setup(self): - "Setting up for related model tests." - for name, state, lon, lat in cities: - loc = Location.objects.create(point=Point(lon, lat)) - c = City.objects.create(name=name, state=state, location=loc) +class RelatedGeoModelTest(TestCase): def test02_select_related(self): "Testing `select_related` on geographic models (see #7126)." @@ -25,6 +15,13 @@ def test02_select_related(self): qs2 = City.objects.select_related() qs3 = City.objects.select_related('location') + # Reference data for what's in the fixtures. + cities = ( + ('Aurora', 'TX', -97.516111, 33.058333), + ('Roswell', 'NM', -104.528056, 33.387222), + ('Kecksburg', 'PA', -79.460734, 40.18476), + ) + for qs in (qs1, qs2, qs3): for ref, c in zip(cities, qs): nm, st, lon, lat = ref @@ -63,11 +60,11 @@ def test04a_related_extent_aggregate(self): # This combines the Extent and Union aggregates into one query aggs = City.objects.aggregate(Extent('location__point')) - # One for all locations, one that excludes Roswell. - all_extent = (-104.528060913086, 33.0583305358887,-79.4607315063477, 40.1847610473633) - txpa_extent = (-97.51611328125, 33.0583305358887,-79.4607315063477, 40.1847610473633) + # One for all locations, one that excludes New Mexico (Roswell). + all_extent = (-104.528056, 29.763374, -79.460734, 40.18476) + txpa_extent = (-97.516111, 29.763374, -79.460734, 40.18476) e1 = City.objects.extent(field_name='location__point') - e2 = City.objects.exclude(name='Roswell').extent(field_name='location__point') + e2 = City.objects.exclude(state='NM').extent(field_name='location__point') e3 = aggs['location__point__extent'] # The tolerance value is to four decimal places because of differences @@ -83,10 +80,12 @@ def test04b_related_union_aggregate(self): aggs = City.objects.aggregate(Union('location__point')) # These are the points that are components of the aggregate geographic - # union that is returned. + # union that is returned. Each point # corresponds to City PK. p1 = Point(-104.528056, 33.387222) p2 = Point(-97.516111, 33.058333) p3 = Point(-79.460734, 40.18476) + p4 = Point(-96.801611, 32.782057) + p5 = Point(-95.363151, 29.763374) # Creating the reference union geometry depending on the spatial backend, # as Oracle will have a different internal ordering of the component @@ -94,14 +93,15 @@ def test04b_related_union_aggregate(self): # query that includes limiting information in the WHERE clause (in other # words a `.filter()` precedes the call to `.unionagg()`). if oracle: - ref_u1 = MultiPoint(p3, p1, p2, srid=4326) + ref_u1 = MultiPoint(p4, p5, p3, p1, p2, srid=4326) ref_u2 = MultiPoint(p3, p2, srid=4326) else: - ref_u1 = MultiPoint(p1, p2, p3, srid=4326) + # Looks like PostGIS points by longitude value. + ref_u1 = MultiPoint(p1, p2, p4, p5, p3, srid=4326) ref_u2 = MultiPoint(p2, p3, srid=4326) u1 = City.objects.unionagg(field_name='location__point') - u2 = City.objects.exclude(name='Roswell').unionagg(field_name='location__point') + u2 = City.objects.exclude(name__in=('Roswell', 'Houston', 'Dallas', 'Fort Worth')).unionagg(field_name='location__point') u3 = aggs['location__point__union'] self.assertEqual(ref_u1, u1) @@ -187,17 +187,10 @@ def test08_defer_only(self): def test09_pk_relations(self): "Ensuring correct primary key column is selected across relations. See #10757." - # Adding two more cities, but this time making sure that their location - # ID values do not match their City ID values. - loc1 = Location.objects.create(point='POINT (-95.363151 29.763374)') - loc2 = Location.objects.create(point='POINT (-96.801611 32.782057)') - dallas = City.objects.create(name='Dallas', state='TX', location=loc2) - houston = City.objects.create(name='Houston', state='TX', location=loc1) - # The expected ID values -- notice the last two location IDs - # are out of order. We want to make sure that the related - # location ID column is selected instead of ID column for - # the city. + # are out of order. Dallas and Houston have location IDs that differ + # from their PKs -- this is done to ensure that the related location + # ID column is selected instead of ID column for the city. city_ids = (1, 2, 3, 4, 5) loc_ids = (1, 2, 3, 5, 4) ids_qs = City.objects.order_by('id').values('id', 'location__id') @@ -232,10 +225,8 @@ def test11_geoquery_pickle(self): @no_oracle def test12a_count(self): "Testing `Count` aggregate use with the `GeoManager` on geo-fields." - # Creating a new City, 'Fort Worth', that uses the same location - # as Dallas. + # The City, 'Fort Worth' uses the same location as Dallas. dallas = City.objects.get(name='Dallas') - ftworth = City.objects.create(name='Fort Worth', state='TX', location=dallas.location) # Count annotation should be 2 for the Dallas location now. loc = Location.objects.annotate(num_cities=Count('city')).get(id=dallas.location.id) @@ -243,18 +234,9 @@ def test12a_count(self): def test12b_count(self): "Testing `Count` aggregate use with the `GeoManager` on non geo-fields. See #11087." - # Creating some data for the Book/Author non-geo models that - # use GeoManager. See #11087. - tp = Author.objects.create(name='Trevor Paglen') - Book.objects.create(title='Torture Taxi', author=tp) - Book.objects.create(title='I Could Tell You But Then You Would Have to be Destroyed by Me', author=tp) - Book.objects.create(title='Blank Spots on the Map', author=tp) - wp = Author.objects.create(name='William Patry') - Book.objects.create(title='Patry on Copyright', author=wp) - # Should only be one author (Trevor Paglen) returned by this query, and - # the annotation should have 3 for the number of books. Also testing - # with a `GeoValuesQuerySet` (see #11489). + # the annotation should have 3 for the number of books, see #11087. + # Also testing with a `GeoValuesQuerySet`, see #11489. qs = Author.objects.annotate(num_books=Count('books')).filter(num_books__gt=1) vqs = Author.objects.values('name').annotate(num_books=Count('books')).filter(num_books__gt=1) self.assertEqual(1, len(qs)) @@ -280,7 +262,7 @@ def test14_collect(self): # SELECT AsText(ST_Collect("relatedapp_location"."point")) FROM "relatedapp_city" LEFT OUTER JOIN # "relatedapp_location" ON ("relatedapp_city"."location_id" = "relatedapp_location"."id") # WHERE "relatedapp_city"."state" = 'TX'; - ref_geom = fromstr('MULTIPOINT(-97.516111 33.058333,-96.801611 32.782057,-95.363151 29.763374,-96.801611 32.782057)') + ref_geom = GEOSGeometry('MULTIPOINT(-97.516111 33.058333,-96.801611 32.782057,-95.363151 29.763374,-96.801611 32.782057)') c1 = City.objects.filter(state='TX').collect(field_name='location__point') c2 = City.objects.filter(state='TX').aggregate(Collect('location__point'))['location__point__collect'] @@ -291,9 +273,12 @@ def test14_collect(self): self.assertEqual(4, len(coll)) self.assertEqual(ref_geom, coll) - # TODO: Related tests for KML, GML, and distance lookups. + def test15_invalid_select_related(self): + "Testing doing select_related on the related name manager of a unique FK. See #13934." + qs = Article.objects.select_related('author__article') + # This triggers TypeError when `get_default_columns` has no `local_only` + # keyword. The TypeError is swallowed if QuerySet is actually + # evaluated as list generation swallows TypeError in CPython. + sql = str(qs.query) -def suite(): - s = unittest.TestSuite() - s.addTest(unittest.makeSuite(RelatedGeoModelTest)) - return s + # TODO: Related tests for KML, GML, and distance lookups. diff --git a/django/contrib/gis/tests/test_geoip.py b/django/contrib/gis/tests/test_geoip.py index 430d61b6d5e2..a9ab6a6ab4d9 100644 --- a/django/contrib/gis/tests/test_geoip.py +++ b/django/contrib/gis/tests/test_geoip.py @@ -5,10 +5,10 @@ # Note: Requires use of both the GeoIP country and city datasets. # The GEOIP_DATA path should be the only setting set (the directory -# should contain links or the actual database files 'GeoIP.dat' and +# should contain links or the actual database files 'GeoIP.dat' and # 'GeoLiteCity.dat'. class GeoIPTest(unittest.TestCase): - + def test01_init(self): "Testing GeoIP initialization." g1 = GeoIP() # Everything inferred from GeoIP path @@ -19,7 +19,7 @@ def test01_init(self): for g in (g1, g2, g3): self.assertEqual(True, bool(g._country)) self.assertEqual(True, bool(g._city)) - + # Only passing in the location of one database. city = os.path.join(path, 'GeoLiteCity.dat') cntry = os.path.join(path, 'GeoIP.dat') @@ -52,10 +52,10 @@ def test02_bad_query(self): def test03_country(self): "Testing GeoIP country querying methods." g = GeoIP(city='') - + fqdn = 'www.google.com' addr = '12.215.42.19' - + for query in (fqdn, addr): for func in (g.country_code, g.country_code_by_addr, g.country_code_by_name): self.assertEqual('US', func(query)) @@ -67,7 +67,7 @@ def test03_country(self): def test04_city(self): "Testing GeoIP city querying methods." g = GeoIP(country='') - + addr = '130.80.29.3' fqdn = 'chron.com' for query in (fqdn, addr): @@ -78,7 +78,7 @@ def test04_city(self): self.assertEqual('United States', func(query)) self.assertEqual({'country_code' : 'US', 'country_name' : 'United States'}, g.country(query)) - + # City information dictionary. d = g.city(query) self.assertEqual('USA', d['country_code3']) @@ -87,7 +87,7 @@ def test04_city(self): self.assertEqual(713, d['area_code']) geom = g.geos(query) self.failIf(not isinstance(geom, GEOSGeometry)) - lon, lat = (-95.4152, 29.7755) + lon, lat = (-95.3670, 29.7523) lat_lon = g.lat_lon(query) lat_lon = (lat_lon[1], lat_lon[0]) for tup in (geom.tuple, g.coords(query), g.lon_lat(query), lat_lon): diff --git a/django/contrib/gis/tests/test_spatialrefsys.py b/django/contrib/gis/tests/test_spatialrefsys.py index a9fcbffee777..c0a72ece6034 100644 --- a/django/contrib/gis/tests/test_spatialrefsys.py +++ b/django/contrib/gis/tests/test_spatialrefsys.py @@ -1,6 +1,7 @@ import unittest from django.db import connection +from django.contrib.gis.gdal import GDAL_VERSION from django.contrib.gis.tests.utils import mysql, no_mysql, oracle, postgis, spatialite test_srs = ({'srid' : 4326, @@ -78,7 +79,8 @@ def test02_osr(self): # Testing the SpatialReference object directly. if postgis or spatialite: srs = sr.srs - self.assertEqual(sd['proj4'], srs.proj4) + if GDAL_VERSION <= (1, 8): + self.assertEqual(sd['proj4'], srs.proj4) # No `srtext` field in the `spatial_ref_sys` table in SpatiaLite if not spatialite: if connection.ops.spatial_version >= (1, 4, 0): diff --git a/django/contrib/gis/tests/urls.py b/django/contrib/gis/tests/urls.py deleted file mode 100644 index 95e36c22a480..000000000000 --- a/django/contrib/gis/tests/urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.conf.urls.defaults import * - -urlpatterns = patterns('', - (r'^geoapp/', include('django.contrib.gis.tests.geoapp.urls')), - ) - diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index cec198979683..945c745be4fd 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -292,7 +292,10 @@ def feature_kwargs(self, feat): if isinstance(model_field, GeometryField): # Verify OGR geometry. - val = self.verify_geom(feat.geom, model_field) + try: + val = self.verify_geom(feat.geom, model_field) + except OGRException: + raise LayerMapError('Could not retrieve geometry from feature.') elif isinstance(model_field, models.base.ModelBase): # The related _model_, not a field was passed in -- indicating # another mapping for the related Model. diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py index 3e19fed823d8..cfa93fe5346d 100644 --- a/django/contrib/humanize/templatetags/humanize.py +++ b/django/contrib/humanize/templatetags/humanize.py @@ -43,7 +43,10 @@ def intword(value): numbers over 1 million. For example, 1000000 becomes '1.0 million', 1200000 becomes '1.2 million' and '1200000000' becomes '1.2 billion'. """ - value = int(value) + try: + value = int(value) + except (TypeError, ValueError): + return value if value < 1000000: return value if value < 1000000000: @@ -66,7 +69,7 @@ def apnumber(value): """ try: value = int(value) - except ValueError: + except (TypeError, ValueError): return value if not 0 < value < 10: return value diff --git a/django/contrib/localflavor/at/forms.py b/django/contrib/localflavor/at/forms.py index e428fdaa171e..cfe02063c2b0 100644 --- a/django/contrib/localflavor/at/forms.py +++ b/django/contrib/localflavor/at/forms.py @@ -4,12 +4,14 @@ import re -from django.utils.translation import ugettext_lazy as _ -from django.forms.fields import Field, RegexField, Select +from django.core.validators import EMPTY_VALUES from django.forms import ValidationError +from django.forms.fields import Field, RegexField, Select +from django.utils.translation import ugettext_lazy as _ re_ssn = re.compile(r'^\d{4} \d{6}') + class ATZipCodeField(RegexField): """ A form field that validates its input is an Austrian postcode. @@ -49,6 +51,9 @@ class ATSocialSecurityNumberField(Field): } def clean(self, value): + value = super(ATSocialSecurityNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u"" if not re_ssn.search(value): raise ValidationError(self.error_messages['invalid']) sqnr, date = value.split(" ") @@ -62,4 +67,3 @@ def clean(self, value): if res != int(check): raise ValidationError(self.error_messages['invalid']) return u'%s%s %s'%(sqnr, check, date,) - diff --git a/django/contrib/localflavor/cl/forms.py b/django/contrib/localflavor/cl/forms.py index 48219e88afdd..23a2209a1066 100644 --- a/django/contrib/localflavor/cl/forms.py +++ b/django/contrib/localflavor/cl/forms.py @@ -74,7 +74,7 @@ def _canonify(self, rut): tuple. """ rut = smart_unicode(rut).replace(' ', '').replace('.', '').replace('-', '') - return rut[:-1], rut[-1] + return rut[:-1], rut[-1].upper() def _format(self, code, verifier=None): """ diff --git a/django/contrib/localflavor/cz/forms.py b/django/contrib/localflavor/cz/forms.py index e980569c70df..a4c380cdd0c6 100644 --- a/django/contrib/localflavor/cz/forms.py +++ b/django/contrib/localflavor/cz/forms.py @@ -51,7 +51,7 @@ class CZBirthNumberField(Field): } def clean(self, value, gender=None): - super(CZBirthNumberField, self).__init__(value) + super(CZBirthNumberField, self).clean(value) if value in EMPTY_VALUES: return u'' @@ -108,7 +108,7 @@ class CZICNumberField(Field): } def clean(self, value): - super(CZICNumberField, self).__init__(value) + super(CZICNumberField, self).clean(value) if value in EMPTY_VALUES: return u'' @@ -138,4 +138,3 @@ def clean(self, value): return u'%s' % value raise ValidationError(self.error_messages['invalid']) - diff --git a/django/contrib/localflavor/in_/in_states.py b/django/contrib/localflavor/in_/in_states.py index 498efe706988..bb4a7482cac1 100644 --- a/django/contrib/localflavor/in_/in_states.py +++ b/django/contrib/localflavor/in_/in_states.py @@ -7,43 +7,43 @@ """ STATE_CHOICES = ( - 'KA', 'Karnataka', - 'AP', 'Andhra Pradesh', - 'KL', 'Kerala', - 'TN', 'Tamil Nadu', - 'MH', 'Maharashtra', - 'UP', 'Uttar Pradesh', - 'GA', 'Goa', - 'GJ', 'Gujarat', - 'RJ', 'Rajasthan', - 'HP', 'Himachal Pradesh', - 'JK', 'Jammu and Kashmir', - 'AR', 'Arunachal Pradesh', - 'AS', 'Assam', - 'BR', 'Bihar', - 'CG', 'Chattisgarh', - 'HR', 'Haryana', - 'JH', 'Jharkhand', - 'MP', 'Madhya Pradesh', - 'MN', 'Manipur', - 'ML', 'Meghalaya', - 'MZ', 'Mizoram', - 'NL', 'Nagaland', - 'OR', 'Orissa', - 'PB', 'Punjab', - 'SK', 'Sikkim', - 'TR', 'Tripura', - 'UA', 'Uttarakhand', - 'WB', 'West Bengal', + ('KA', 'Karnataka'), + ('AP', 'Andhra Pradesh'), + ('KL', 'Kerala'), + ('TN', 'Tamil Nadu'), + ('MH', 'Maharashtra'), + ('UP', 'Uttar Pradesh'), + ('GA', 'Goa'), + ('GJ', 'Gujarat'), + ('RJ', 'Rajasthan'), + ('HP', 'Himachal Pradesh'), + ('JK', 'Jammu and Kashmir'), + ('AR', 'Arunachal Pradesh'), + ('AS', 'Assam'), + ('BR', 'Bihar'), + ('CG', 'Chattisgarh'), + ('HR', 'Haryana'), + ('JH', 'Jharkhand'), + ('MP', 'Madhya Pradesh'), + ('MN', 'Manipur'), + ('ML', 'Meghalaya'), + ('MZ', 'Mizoram'), + ('NL', 'Nagaland'), + ('OR', 'Orissa'), + ('PB', 'Punjab'), + ('SK', 'Sikkim'), + ('TR', 'Tripura'), + ('UA', 'Uttarakhand'), + ('WB', 'West Bengal'), # Union Territories - 'AN', 'Andaman and Nicobar', - 'CH', 'Chandigarh', - 'DN', 'Dadra and Nagar Haveli', - 'DD', 'Daman and Diu', - 'DL', 'Delhi', - 'LD', 'Lakshadweep', - 'PY', 'Pondicherry', + ('AN', 'Andaman and Nicobar'), + ('CH', 'Chandigarh'), + ('DN', 'Dadra and Nagar Haveli'), + ('DD', 'Daman and Diu'), + ('DL', 'Delhi'), + ('LD', 'Lakshadweep'), + ('PY', 'Pondicherry'), ) STATES_NORMALIZED = { diff --git a/django/contrib/localflavor/it/forms.py b/django/contrib/localflavor/it/forms.py index baa56a21bae2..bf0227608ae0 100644 --- a/django/contrib/localflavor/it/forms.py +++ b/django/contrib/localflavor/it/forms.py @@ -50,8 +50,8 @@ def __init__(self, *args, **kwargs): def clean(self, value): value = super(ITSocialSecurityNumberField, self).clean(value) - if value == u'': - return value + if value in EMPTY_VALUES: + return u'' value = re.sub('\s', u'', value).upper() try: check_digit = ssn_check_digit(value) @@ -71,8 +71,8 @@ class ITVatNumberField(Field): def clean(self, value): value = super(ITVatNumberField, self).clean(value) - if value == u'': - return value + if value in EMPTY_VALUES: + return u'' try: vat_number = int(value) except ValueError: diff --git a/django/contrib/localflavor/pl/forms.py b/django/contrib/localflavor/pl/forms.py index b9085805684f..d1e9773a804b 100644 --- a/django/contrib/localflavor/pl/forms.py +++ b/django/contrib/localflavor/pl/forms.py @@ -7,6 +7,7 @@ from django.forms import ValidationError from django.forms.fields import Select, RegexField from django.utils.translation import ugettext_lazy as _ +from django.core.validators import EMPTY_VALUES class PLProvinceSelect(Select): """ @@ -45,6 +46,8 @@ def __init__(self, *args, **kwargs): def clean(self,value): super(PLPESELField, self).clean(value) + if value in EMPTY_VALUES: + return u'' if not self.has_valid_checksum(value): raise ValidationError(self.error_messages['checksum']) return u'%s' % value @@ -78,6 +81,8 @@ def __init__(self, *args, **kwargs): def clean(self,value): super(PLNIPField, self).clean(value) + if value in EMPTY_VALUES: + return u'' value = re.sub("[-]", "", value) if not self.has_valid_checksum(value): raise ValidationError(self.error_messages['checksum']) @@ -116,6 +121,8 @@ def __init__(self, *args, **kwargs): def clean(self,value): super(PLREGONField, self).clean(value) + if value in EMPTY_VALUES: + return u'' if not self.has_valid_checksum(value): raise ValidationError(self.error_messages['checksum']) return u'%s' % value diff --git a/django/contrib/localflavor/ro/forms.py b/django/contrib/localflavor/ro/forms.py index dd86fce9f21b..a218bfd167b9 100644 --- a/django/contrib/localflavor/ro/forms.py +++ b/django/contrib/localflavor/ro/forms.py @@ -20,7 +20,7 @@ class ROCIFField(RegexField): } def __init__(self, *args, **kwargs): - super(ROCIFField, self).__init__(r'^[0-9]{2,10}', max_length=10, + super(ROCIFField, self).__init__(r'^(RO)?[0-9]{2,10}', max_length=10, min_length=2, *args, **kwargs) def clean(self, value): @@ -65,6 +65,8 @@ def clean(self, value): CNP validations """ value = super(ROCNPField, self).clean(value) + if value in EMPTY_VALUES: + return u'' # check birthdate digits import datetime try: @@ -150,6 +152,8 @@ def clean(self, value): Strips - and spaces, performs country code and checksum validation """ value = super(ROIBANField, self).clean(value) + if value in EMPTY_VALUES: + return u'' value = value.replace('-','') value = value.replace(' ','') value = value.upper() @@ -180,6 +184,8 @@ def clean(self, value): Strips -, (, ) and spaces. Checks the final length. """ value = super(ROPhoneNumberField, self).clean(value) + if value in EMPTY_VALUES: + return u'' value = value.replace('-','') value = value.replace('(','') value = value.replace(')','') diff --git a/django/contrib/localflavor/se/utils.py b/django/contrib/localflavor/se/utils.py index 7fe2b098124f..50d8ac1d8e56 100644 --- a/django/contrib/localflavor/se/utils.py +++ b/django/contrib/localflavor/se/utils.py @@ -12,7 +12,7 @@ def id_number_checksum(gd): if tmp > 9: tmp = sum([int(i) for i in str(tmp)]) - + s += tmp n += 1 @@ -28,9 +28,9 @@ def validate_id_birthday(gd, fix_coordination_number_day=True): If the date is an invalid birth day, a ValueError will be raised. """ - + today = datetime.date.today() - + day = int(gd['day']) if fix_coordination_number_day and day > 60: day -= 60 @@ -40,7 +40,7 @@ def validate_id_birthday(gd, fix_coordination_number_day=True): # The century was not specified, and need to be calculated from todays date current_year = today.year year = int(today.strftime('%Y')) - int(today.strftime('%y')) + int(gd['year']) - + if ('%s%s%02d' % (gd['year'], gd['month'], day)) > today.strftime('%y%m%d'): year -= 100 @@ -49,7 +49,7 @@ def validate_id_birthday(gd, fix_coordination_number_day=True): year -= 100 else: year = int(gd['century'] + gd['year']) - + # Make sure the year is valid # There are no swedish personal identity numbers where year < 1800 if year < 1800: @@ -57,11 +57,11 @@ def validate_id_birthday(gd, fix_coordination_number_day=True): # ValueError will be raise for invalid dates birth_day = datetime.date(year, int(gd['month']), day) - + # birth_day must not be in the future if birth_day > today: raise ValueError - + return birth_day def format_personal_id_number(birth_day, gd): diff --git a/django/contrib/localflavor/za/forms.py b/django/contrib/localflavor/za/forms.py index 9a54f1ecb271..4fb4203e455a 100644 --- a/django/contrib/localflavor/za/forms.py +++ b/django/contrib/localflavor/za/forms.py @@ -22,14 +22,14 @@ class ZAIDField(Field): } def clean(self, value): - # strip spaces and dashes - value = value.strip().replace(' ', '').replace('-', '') - super(ZAIDField, self).clean(value) if value in EMPTY_VALUES: return u'' + # strip spaces and dashes + value = value.strip().replace(' ', '').replace('-', '') + match = re.match(id_re, value) if not match: @@ -57,4 +57,4 @@ class ZAPostCodeField(RegexField): def __init__(self, *args, **kwargs): super(ZAPostCodeField, self).__init__(r'^\d{4}$', - max_length=None, min_length=None) + max_length=None, min_length=None, *args, **kwargs) diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 912655f83b78..7cdc04c65383 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -8,7 +8,7 @@ * Markdown, which requires the Python-markdown library from http://www.freewisdom.org/projects/python-markdown - * ReStructuredText, which requires docutils from http://docutils.sf.net/ + * reStructuredText, which requires docutils from http://docutils.sf.net/ """ from django import template diff --git a/django/contrib/messages/tests/base.py b/django/contrib/messages/tests/base.py index 2dea5a46d0b6..6a7700b3ab5d 100644 --- a/django/contrib/messages/tests/base.py +++ b/django/contrib/messages/tests/base.py @@ -49,6 +49,8 @@ def setUp(self): self._message_storage = settings.MESSAGE_STORAGE settings.MESSAGE_STORAGE = '%s.%s' % (self.storage_class.__module__, self.storage_class.__name__) + self.old_TEMPLATE_DIRS = settings.TEMPLATE_DIRS + settings.TEMPLATE_DIRS = () def tearDown(self): for setting in self.restore_settings: @@ -59,6 +61,7 @@ def tearDown(self): self._template_context_processors settings.INSTALLED_APPS = self._installed_apps settings.MESSAGE_STORAGE = self._message_storage + settings.TEMPLATE_DIRS = self.old_TEMPLATE_DIRS def restore_setting(self, setting): if setting in self._remembered_settings: diff --git a/django/contrib/messages/tests/urls.py b/django/contrib/messages/tests/urls.py index 6252adcfb191..4808e2218e30 100644 --- a/django/contrib/messages/tests/urls.py +++ b/django/contrib/messages/tests/urls.py @@ -4,8 +4,10 @@ from django.http import HttpResponseRedirect, HttpResponse from django.shortcuts import render_to_response from django.template import RequestContext, Template +from django.views.decorators.cache import never_cache +@never_cache def add(request, message_type): # don't default to False here, because we want to test that it defaults # to False if unspecified @@ -20,6 +22,7 @@ def add(request, message_type): return HttpResponseRedirect(show_url) +@never_cache def show(request): t = Template("""{% if messages %}
          diff --git a/django/contrib/sessions/backends/cache.py b/django/contrib/sessions/backends/cache.py index ab0716dcb446..b326b8b0d42b 100644 --- a/django/contrib/sessions/backends/cache.py +++ b/django/contrib/sessions/backends/cache.py @@ -1,6 +1,8 @@ from django.contrib.sessions.backends.base import SessionBase, CreateError from django.core.cache import cache +KEY_PREFIX = "django.contrib.sessions.cache" + class SessionStore(SessionBase): """ A cache-based session store. @@ -10,7 +12,7 @@ def __init__(self, session_key=None): super(SessionStore, self).__init__(session_key) def load(self): - session_data = self._cache.get(self.session_key) + session_data = self._cache.get(KEY_PREFIX + self.session_key) if session_data is not None: return session_data self.create() @@ -37,13 +39,13 @@ def save(self, must_create=False): func = self._cache.add else: func = self._cache.set - result = func(self.session_key, self._get_session(no_load=must_create), + result = func(KEY_PREFIX + self.session_key, self._get_session(no_load=must_create), self.get_expiry_age()) if must_create and not result: raise CreateError def exists(self, session_key): - if self._cache.has_key(session_key): + if self._cache.has_key(KEY_PREFIX + session_key): return True return False @@ -52,5 +54,5 @@ def delete(self, session_key=None): if self._session_key is None: return session_key = self._session_key - self._cache.delete(session_key) + self._cache.delete(KEY_PREFIX + session_key) diff --git a/django/contrib/sessions/backends/cached_db.py b/django/contrib/sessions/backends/cached_db.py index 9e22c69228f0..465472d29c48 100644 --- a/django/contrib/sessions/backends/cached_db.py +++ b/django/contrib/sessions/backends/cached_db.py @@ -6,6 +6,8 @@ from django.contrib.sessions.backends.db import SessionStore as DBStore from django.core.cache import cache +KEY_PREFIX = "django.contrib.sessions.cached_db" + class SessionStore(DBStore): """ Implements cached, database backed sessions. @@ -15,10 +17,11 @@ def __init__(self, session_key=None): super(SessionStore, self).__init__(session_key) def load(self): - data = cache.get(self.session_key, None) + data = cache.get(KEY_PREFIX + self.session_key, None) if data is None: data = super(SessionStore, self).load() - cache.set(self.session_key, data, settings.SESSION_COOKIE_AGE) + cache.set(KEY_PREFIX + self.session_key, data, + settings.SESSION_COOKIE_AGE) return data def exists(self, session_key): @@ -26,11 +29,12 @@ def exists(self, session_key): def save(self, must_create=False): super(SessionStore, self).save(must_create) - cache.set(self.session_key, self._session, settings.SESSION_COOKIE_AGE) + cache.set(KEY_PREFIX + self.session_key, self._session, + settings.SESSION_COOKIE_AGE) def delete(self, session_key=None): super(SessionStore, self).delete(session_key) - cache.delete(session_key or self.session_key) + cache.delete(KEY_PREFIX + (session_key or self.session_key)) def flush(self): """ @@ -39,4 +43,4 @@ def flush(self): """ self.clear() self.delete(self.session_key) - self.create() \ No newline at end of file + self.create() diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py index 3f6350345fa0..c3516ea32805 100644 --- a/django/contrib/sessions/backends/file.py +++ b/django/contrib/sessions/backends/file.py @@ -26,6 +26,8 @@ def __init__(self, session_key=None): self.file_prefix = settings.SESSION_COOKIE_NAME super(SessionStore, self).__init__(session_key) + VALID_KEY_CHARS = set("abcdef0123456789") + def _key_to_file(self, session_key=None): """ Get the file associated with this session key. @@ -36,9 +38,9 @@ def _key_to_file(self, session_key=None): # Make sure we're not vulnerable to directory traversal. Session keys # should always be md5s, so they should never contain directory # components. - if os.path.sep in session_key: + if not set(session_key).issubset(self.VALID_KEY_CHARS): raise SuspiciousOperation( - "Invalid characters (directory components) in session key") + "Invalid characters in session key") return os.path.join(self.storage_path, self.file_prefix + session_key) diff --git a/django/contrib/sessions/models.py b/django/contrib/sessions/models.py index c3b72e6eafac..4c76ddf09a0e 100644 --- a/django/contrib/sessions/models.py +++ b/django/contrib/sessions/models.py @@ -40,7 +40,7 @@ class Session(models.Model): For complete documentation on using Sessions in your code, consult the sessions documentation that is shipped with Django (also available - on the Django website). + on the Django Web site). """ session_key = models.CharField(_('session key'), max_length=40, primary_key=True) diff --git a/django/contrib/sessions/tests.py b/django/contrib/sessions/tests.py index f0a3c4ec8c4f..01faab859cc3 100644 --- a/django/contrib/sessions/tests.py +++ b/django/contrib/sessions/tests.py @@ -129,6 +129,17 @@ >>> file_session = FileSession(file_session.session_key) >>> file_session.save() +# Ensure we don't allow directory traversal +>>> FileSession("a/b/c").load() +Traceback (innermost last): + ... +SuspiciousOperation: Invalid characters in session key + +>>> FileSession("a\\b\\c").load() +Traceback (innermost last): + ... +SuspiciousOperation: Invalid characters in session key + # Make sure the file backend checks for a good storage dir >>> settings.SESSION_FILE_PATH = "/if/this/directory/exists/you/have/a/weird/computer" >>> FileSession() diff --git a/django/contrib/sitemaps/__init__.py b/django/contrib/sitemaps/__init__.py index f877317f167a..6b8d5a03d1c6 100644 --- a/django/contrib/sitemaps/__init__.py +++ b/django/contrib/sitemaps/__init__.py @@ -1,4 +1,6 @@ +from django.contrib.sites.models import Site, get_current_site from django.core import urlresolvers, paginator +from django.core.exceptions import ImproperlyConfigured import urllib PING_URL = "http://www.google.com/webmasters/tools/ping" @@ -59,26 +61,33 @@ def _get_paginator(self): return self._paginator paginator = property(_get_paginator) - def get_urls(self, page=1): - from django.contrib.sites.models import Site - current_site = Site.objects.get_current() + def get_urls(self, page=1, site=None): + if site is None: + if Site._meta.installed: + try: + site = Site.objects.get_current() + except Site.DoesNotExist: + pass + if site is None: + raise ImproperlyConfigured("In order to use Sitemaps you must either use the sites framework or pass in a Site or RequestSite object in your view code.") + urls = [] for item in self.paginator.page(page).object_list: - loc = "http://%s%s" % (current_site.domain, self.__get('location', item)) + loc = "http://%s%s" % (site.domain, self.__get('location', item)) + priority = self.__get('priority', item, None) url_info = { 'location': loc, 'lastmod': self.__get('lastmod', item, None), 'changefreq': self.__get('changefreq', item, None), - 'priority': self.__get('priority', item, None) + 'priority': str(priority is not None and priority or '') } urls.append(url_info) return urls class FlatPageSitemap(Sitemap): def items(self): - from django.contrib.sites.models import Site current_site = Site.objects.get_current() - return current_site.flatpage_set.all() + return current_site.flatpage_set.filter(registration_required=False) class GenericSitemap(Sitemap): priority = None diff --git a/django/contrib/sitemaps/management/commands/ping_google.py b/django/contrib/sitemaps/management/commands/ping_google.py index afff04b39c6e..7fa1690a1475 100644 --- a/django/contrib/sitemaps/management/commands/ping_google.py +++ b/django/contrib/sitemaps/management/commands/ping_google.py @@ -3,7 +3,7 @@ class Command(BaseCommand): - help = "Ping google with an updated sitemap, pass optional url of sitemap" + help = "Ping Google with an updated sitemap, pass optional url of sitemap" def execute(self, *args, **options): if len(args) == 1: diff --git a/django/contrib/sitemaps/models.py b/django/contrib/sitemaps/models.py new file mode 100644 index 000000000000..7ff128fa692e --- /dev/null +++ b/django/contrib/sitemaps/models.py @@ -0,0 +1 @@ +# This file intentionally left blank \ No newline at end of file diff --git a/django/contrib/sitemaps/tests/__init__.py b/django/contrib/sitemaps/tests/__init__.py new file mode 100644 index 000000000000..c5b483cde2c8 --- /dev/null +++ b/django/contrib/sitemaps/tests/__init__.py @@ -0,0 +1 @@ +from django.contrib.sitemaps.tests.basic import * diff --git a/django/contrib/sitemaps/tests/basic.py b/django/contrib/sitemaps/tests/basic.py new file mode 100644 index 000000000000..51ece59271b7 --- /dev/null +++ b/django/contrib/sitemaps/tests/basic.py @@ -0,0 +1,129 @@ +from datetime import date +from django.conf import settings +from django.contrib.auth.models import User +from django.contrib.sitemaps import Sitemap +from django.contrib.sites.models import Site +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase +from django.utils.formats import localize +from django.utils.translation import activate, deactivate + + +class SitemapTests(TestCase): + urls = 'django.contrib.sitemaps.tests.urls' + + def setUp(self): + if Site._meta.installed: + self.base_url = 'http://example.com' + else: + self.base_url = 'http://testserver' + self.old_USE_L10N = settings.USE_L10N + self.old_Site_meta_installed = Site._meta.installed + # Create a user that will double as sitemap content + User.objects.create_user('testuser', 'test@example.com', 's3krit') + + def tearDown(self): + settings.USE_L10N = self.old_USE_L10N + Site._meta.installed = self.old_Site_meta_installed + + def test_simple_sitemap(self): + "A simple sitemap can be rendered" + # Retrieve the sitemap. + response = self.client.get('/simple/sitemap.xml') + # Check for all the important bits: + self.assertEquals(response.content, """ + +%s/location/%snever0.5 + +""" % (self.base_url, date.today().strftime('%Y-%m-%d'))) + + if settings.USE_I18N: + def test_localized_priority(self): + "The priority value should not be localized (Refs #14164)" + # Localization should be active + settings.USE_L10N = True + activate('fr') + self.assertEqual(u'0,3', localize(0.3)) + + # Retrieve the sitemap. Check that priorities + # haven't been rendered in localized format + response = self.client.get('/simple/sitemap.xml') + self.assertContains(response, '0.5') + self.assertContains(response, '%s' % date.today().strftime('%Y-%m-%d')) + deactivate() + + def test_generic_sitemap(self): + "A minimal generic sitemap can be rendered" + # Retrieve the sitemap. + response = self.client.get('/generic/sitemap.xml') + + expected = '' + for username in User.objects.values_list("username", flat=True): + expected += "%s/users/%s/" % (self.base_url, username) + # Check for all the important bits: + self.assertEquals(response.content, """ + +%s + +""" % expected) + + if "django.contrib.flatpages" in settings.INSTALLED_APPS: + def test_flatpage_sitemap(self): + "Basic FlatPage sitemap test" + + # Import FlatPage inside the test so that when django.contrib.flatpages + # is not installed we don't get problems trying to delete Site + # objects (FlatPage has an M2M to Site, Site.delete() tries to + # delete related objects, but the M2M table doesn't exist. + from django.contrib.flatpages.models import FlatPage + + public = FlatPage.objects.create( + url=u'/public/', + title=u'Public Page', + enable_comments=True, + registration_required=False, + ) + public.sites.add(settings.SITE_ID) + private = FlatPage.objects.create( + url=u'/private/', + title=u'Private Page', + enable_comments=True, + registration_required=True + ) + private.sites.add(settings.SITE_ID) + response = self.client.get('/flatpages/sitemap.xml') + # Public flatpage should be in the sitemap + self.assertContains(response, '%s%s' % (self.base_url, public.url)) + # Private flatpage should not be in the sitemap + self.assertNotContains(response, '%s%s' % (self.base_url, private.url)) + + def test_requestsite_sitemap(self): + # Make sure hitting the flatpages sitemap without the sites framework + # installed doesn't raise an exception + Site._meta.installed = False + # Retrieve the sitemap. + response = self.client.get('/simple/sitemap.xml') + # Check for all the important bits: + self.assertEquals(response.content, """ + +http://testserver/location/%snever0.5 + +""" % date.today().strftime('%Y-%m-%d')) + + if "django.contrib.sites" in settings.INSTALLED_APPS: + def test_sitemap_get_urls_no_site_1(self): + """ + Check we get ImproperlyConfigured if we don't pass a site object to + Sitemap.get_urls and no Site objects exist + """ + Site.objects.all().delete() + self.assertRaises(ImproperlyConfigured, Sitemap().get_urls) + + def test_sitemap_get_urls_no_site_2(self): + """ + Check we get ImproperlyConfigured when we don't pass a site object to + Sitemap.get_urls if Site objects exists, but the sites framework is not + actually installed. + """ + Site._meta.installed = False + self.assertRaises(ImproperlyConfigured, Sitemap().get_urls) diff --git a/django/contrib/sitemaps/tests/urls.py b/django/contrib/sitemaps/tests/urls.py new file mode 100644 index 000000000000..6cdba36b0253 --- /dev/null +++ b/django/contrib/sitemaps/tests/urls.py @@ -0,0 +1,33 @@ +from datetime import datetime +from django.conf.urls.defaults import * +from django.contrib.sitemaps import Sitemap, GenericSitemap, FlatPageSitemap +from django.contrib.auth.models import User + +class SimpleSitemap(Sitemap): + changefreq = "never" + priority = 0.5 + location = '/location/' + lastmod = datetime.now() + + def items(self): + return [object()] + +simple_sitemaps = { + 'simple': SimpleSitemap, +} + +generic_sitemaps = { + 'generic': GenericSitemap({ + 'queryset': User.objects.all() + }), +} + +flatpage_sitemaps = { + 'flatpages': FlatPageSitemap, +} + +urlpatterns = patterns('django.contrib.sitemaps.views', + (r'^simple/sitemap\.xml$', 'sitemap', {'sitemaps': simple_sitemaps}), + (r'^generic/sitemap\.xml$', 'sitemap', {'sitemaps': generic_sitemaps}), + (r'^flatpages/sitemap\.xml$', 'sitemap', {'sitemaps': flatpage_sitemaps}), +) diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py index 7a5fe38a0817..b7a96e12aafd 100644 --- a/django/contrib/sitemaps/views.py +++ b/django/contrib/sitemaps/views.py @@ -1,15 +1,16 @@ from django.http import HttpResponse, Http404 from django.template import loader -from django.contrib.sites.models import Site +from django.contrib.sites.models import get_current_site from django.core import urlresolvers from django.utils.encoding import smart_str from django.core.paginator import EmptyPage, PageNotAnInteger def index(request, sitemaps): - current_site = Site.objects.get_current() + current_site = get_current_site(request) sites = [] protocol = request.is_secure() and 'https' or 'http' for section, site in sitemaps.items(): + site.request = request if callable(site): pages = site().paginator.num_pages else: @@ -31,12 +32,13 @@ def sitemap(request, sitemaps, section=None): else: maps = sitemaps.values() page = request.GET.get("p", 1) + current_site = get_current_site(request) for site in maps: try: if callable(site): - urls.extend(site().get_urls(page)) + urls.extend(site().get_urls(page=page, site=current_site)) else: - urls.extend(site.get_urls(page)) + urls.extend(site.get_urls(page=page, site=current_site)) except EmptyPage: raise Http404("Page %s empty" % page) except PageNotAnInteger: diff --git a/django/contrib/sites/managers.py b/django/contrib/sites/managers.py index 59215c44f92f..3df485a04008 100644 --- a/django/contrib/sites/managers.py +++ b/django/contrib/sites/managers.py @@ -4,17 +4,38 @@ class CurrentSiteManager(models.Manager): "Use this to limit objects to those associated with the current site." - def __init__(self, field_name='site'): + def __init__(self, field_name=None): super(CurrentSiteManager, self).__init__() self.__field_name = field_name self.__is_validated = False - + + def _validate_field_name(self): + field_names = self.model._meta.get_all_field_names() + + # If a custom name is provided, make sure the field exists on the model + if self.__field_name is not None and self.__field_name not in field_names: + raise ValueError("%s couldn't find a field named %s in %s." % \ + (self.__class__.__name__, self.__field_name, self.model._meta.object_name)) + + # Otherwise, see if there is a field called either 'site' or 'sites' + else: + for potential_name in ['site', 'sites']: + if potential_name in field_names: + self.__field_name = potential_name + self.__is_validated = True + break + + # Now do a type check on the field (FK or M2M only) + try: + field = self.model._meta.get_field(self.__field_name) + if not isinstance(field, (models.ForeignKey, models.ManyToManyField)): + raise TypeError("%s must be a ForeignKey or ManyToManyField." %self.__field_name) + except FieldDoesNotExist: + raise ValueError("%s couldn't find a field named %s in %s." % \ + (self.__class__.__name__, self.__field_name, self.model._meta.object_name)) + self.__is_validated = True + def get_query_set(self): if not self.__is_validated: - try: - self.model._meta.get_field(self.__field_name) - except FieldDoesNotExist: - raise ValueError("%s couldn't find a field named %s in %s." % \ - (self.__class__.__name__, self.__field_name, self.model._meta.object_name)) - self.__is_validated = True + self._validate_field_name() return super(CurrentSiteManager, self).get_query_set().filter(**{self.__field_name + '__id__exact': settings.SITE_ID}) diff --git a/django/contrib/sites/models.py b/django/contrib/sites/models.py index 9b1697e251ab..fecbff79d802 100644 --- a/django/contrib/sites/models.py +++ b/django/contrib/sites/models.py @@ -1,9 +1,12 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ + SITE_CACHE = {} + class SiteManager(models.Manager): + def get_current(self): """ Returns the current ``Site`` based on the SITE_ID in the @@ -28,7 +31,9 @@ def clear_cache(self): global SITE_CACHE SITE_CACHE = {} + class Site(models.Model): + domain = models.CharField(_('domain name'), max_length=100) name = models.CharField(_('display name'), max_length=50) objects = SiteManager() @@ -56,6 +61,7 @@ def delete(self): except KeyError: pass + class RequestSite(object): """ A class that shares the primary interface of Site (i.e., it has @@ -75,3 +81,15 @@ def save(self, force_insert=False, force_update=False): def delete(self): raise NotImplementedError('RequestSite cannot be deleted.') + + +def get_current_site(request): + """ + Checks if contrib.sites is installed and returns either the current + ``Site`` object or a ``RequestSite`` object based on the request. + """ + if Site._meta.installed: + current_site = Site.objects.get_current() + else: + current_site = RequestSite(request) + return current_site diff --git a/django/contrib/sites/tests.py b/django/contrib/sites/tests.py index e3fa81b9d11a..85cb53c403b0 100644 --- a/django/contrib/sites/tests.py +++ b/django/contrib/sites/tests.py @@ -1,29 +1,56 @@ -""" ->>> from django.contrib.sites.models import Site ->>> from django.conf import settings ->>> Site(id=settings.SITE_ID, domain="example.com", name="example.com").save() - -# Make sure that get_current() does not return a deleted Site object. ->>> s = Site.objects.get_current() ->>> isinstance(s, Site) -True - ->>> s.delete() ->>> Site.objects.get_current() -Traceback (most recent call last): -... -DoesNotExist: Site matching query does not exist. - -# After updating a Site object (e.g. via the admin), we shouldn't return a -# bogus value from the SITE_CACHE. ->>> _ = Site.objects.create(id=settings.SITE_ID, domain="example.com", name="example.com") ->>> site = Site.objects.get_current() ->>> site.name -u"example.com" ->>> s2 = Site.objects.get(id=settings.SITE_ID) ->>> s2.name = "Example site" ->>> s2.save() ->>> site = Site.objects.get_current() ->>> site.name -u"Example site" -""" +from django.conf import settings +from django.contrib.sites.models import Site, RequestSite, get_current_site +from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpRequest +from django.test import TestCase + + +class SitesFrameworkTests(TestCase): + + def setUp(self): + Site(id=settings.SITE_ID, domain="example.com", name="example.com").save() + self.old_Site_meta_installed = Site._meta.installed + Site._meta.installed = True + + def tearDown(self): + Site._meta.installed = self.old_Site_meta_installed + + def test_site_manager(self): + # Make sure that get_current() does not return a deleted Site object. + s = Site.objects.get_current() + self.assert_(isinstance(s, Site)) + s.delete() + self.assertRaises(ObjectDoesNotExist, Site.objects.get_current) + + def test_site_cache(self): + # After updating a Site object (e.g. via the admin), we shouldn't return a + # bogus value from the SITE_CACHE. + site = Site.objects.get_current() + self.assertEqual(u"example.com", site.name) + s2 = Site.objects.get(id=settings.SITE_ID) + s2.name = "Example site" + s2.save() + site = Site.objects.get_current() + self.assertEqual(u"Example site", site.name) + + def test_get_current_site(self): + # Test that the correct Site object is returned + request = HttpRequest() + request.META = { + "SERVER_NAME": "example.com", + "SERVER_PORT": "80", + } + site = get_current_site(request) + self.assert_(isinstance(site, Site)) + self.assertEqual(site.id, settings.SITE_ID) + + # Test that an exception is raised if the sites framework is installed + # but there is no matching Site + site.delete() + self.assertRaises(ObjectDoesNotExist, get_current_site, request) + + # A RequestSite is returned if the sites framework is not installed + Site._meta.installed = False + site = get_current_site(request) + self.assert_(isinstance(site, RequestSite)) + self.assertEqual(site.name, u"example.com") diff --git a/django/contrib/syndication/views.py b/django/contrib/syndication/views.py index b99f3f9affbb..b0462ece5541 100644 --- a/django/contrib/syndication/views.py +++ b/django/contrib/syndication/views.py @@ -1,20 +1,23 @@ -import datetime from django.conf import settings -from django.contrib.sites.models import Site, RequestSite +from django.contrib.sites.models import get_current_site from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.http import HttpResponse, Http404 -from django.template import loader, Template, TemplateDoesNotExist, RequestContext +from django.template import loader, TemplateDoesNotExist, RequestContext from django.utils import feedgenerator, tzinfo from django.utils.encoding import force_unicode, iri_to_uri, smart_unicode from django.utils.html import escape -def add_domain(domain, url): +def add_domain(domain, url, secure=False): if not (url.startswith('http://') or url.startswith('https://') or url.startswith('mailto:')): # 'url' must already be ASCII and URL-quoted, so no need for encoding # conversions here. - url = iri_to_uri(u'http://%s%s' % (domain, url)) + if secure: + protocol = 'https' + else: + protocol = 'http' + url = iri_to_uri(u'%s://%s%s' % (protocol, domain, url)) return url class FeedDoesNotExist(ObjectDoesNotExist): @@ -91,13 +94,10 @@ def get_feed(self, obj, request): Returns a feedgenerator.DefaultFeed object, fully populated, for this feed. Raises FeedDoesNotExist for invalid parameters. """ - if Site._meta.installed: - current_site = Site.objects.get_current() - else: - current_site = RequestSite(request) + current_site = get_current_site(request) link = self.__get_dynamic_attr('link', obj) - link = add_domain(current_site.domain, link) + link = add_domain(current_site.domain, link, request.is_secure()) feed = self.feed_type( title = self.__get_dynamic_attr('title', obj), @@ -105,8 +105,11 @@ def get_feed(self, obj, request): link = link, description = self.__get_dynamic_attr('description', obj), language = settings.LANGUAGE_CODE.decode(), - feed_url = add_domain(current_site.domain, - self.__get_dynamic_attr('feed_url', obj) or request.path), + feed_url = add_domain( + current_site.domain, + self.__get_dynamic_attr('feed_url', obj) or request.path, + request.is_secure(), + ), author_name = self.__get_dynamic_attr('author_name', obj), author_link = self.__get_dynamic_attr('author_link', obj), author_email = self.__get_dynamic_attr('author_email', obj), @@ -140,7 +143,11 @@ def get_feed(self, obj, request): description = description_tmp.render(RequestContext(request, {'obj': item, 'site': current_site})) else: description = self.__get_dynamic_attr('item_description', item) - link = add_domain(current_site.domain, self.__get_dynamic_attr('item_link', item)) + link = add_domain( + current_site.domain, + self.__get_dynamic_attr('item_link', item), + request.is_secure(), + ) enc = None enc_url = self.__get_dynamic_attr('item_enclosure_url', item) if enc_url: @@ -180,6 +187,7 @@ def get_feed(self, obj, request): def feed(request, url, feed_dict=None): """Provided for backwards compatibility.""" + from django.contrib.syndication.feeds import Feed as LegacyFeed import warnings warnings.warn('The syndication feed() view is deprecated. Please use the ' 'new class based view API.', @@ -198,6 +206,18 @@ def feed(request, url, feed_dict=None): except KeyError: raise Http404("Slug %r isn't registered." % slug) + # Backwards compatibility within the backwards compatibility; + # Feeds can be updated to be class-based, but still be deployed + # using the legacy feed view. This only works if the feed takes + # no arguments (i.e., get_object returns None). Refs #14176. + if not issubclass(f, LegacyFeed): + instance = f() + instance.feed_url = getattr(f, 'feed_url', None) or request.path + instance.title_template = f.title_template or ('feeds/%s_title.html' % slug) + instance.description_template = f.description_template or ('feeds/%s_description.html' % slug) + + return instance(request) + try: feedgen = f(slug, request).get_feed(param) except FeedDoesNotExist: diff --git a/django/contrib/webdesign/tests.py b/django/contrib/webdesign/tests.py index d155620902a7..8907ea3ba7bc 100644 --- a/django/contrib/webdesign/tests.py +++ b/django/contrib/webdesign/tests.py @@ -1,20 +1,21 @@ # -*- coding: utf-8 -*- -r""" ->>> words(7) -u'lorem ipsum dolor sit amet consectetur adipisicing' +import unittest ->>> paragraphs(1) -['Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'] +from django.contrib.webdesign.lorem_ipsum import * +from django.template import loader, Context ->>> from django.template import loader, Context ->>> t = loader.get_template_from_string("{% load webdesign %}{% lorem 3 w %}") ->>> t.render(Context({})) -u'lorem ipsum dolor' -""" -from django.contrib.webdesign.lorem_ipsum import * +class WebdesignTest(unittest.TestCase): + + def test_words(self): + self.assertEqual(words(7), u'lorem ipsum dolor sit amet consectetur adipisicing') + + def test_paragraphs(self): + self.assertEqual(paragraphs(1), + ['Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.']) -if __name__ == '__main__': - import doctest - doctest.testmod() + def test_lorem_tag(self): + t = loader.get_template_from_string("{% load webdesign %}{% lorem 3 w %}") + self.assertEqual(t.render(Context({})), + u'lorem ipsum dolor') diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py index 1b602908cbd5..334a932a9886 100644 --- a/django/core/cache/__init__.py +++ b/django/core/cache/__init__.py @@ -15,10 +15,14 @@ See docs/cache.txt for information on the public API. """ -from cgi import parse_qsl +try: + from urlparse import parse_qsl +except ImportError: + from cgi import parse_qsl + from django.conf import settings from django.core import signals -from django.core.cache.backends.base import InvalidCacheBackendError +from django.core.cache.backends.base import InvalidCacheBackendError, CacheKeyWarning from django.utils import importlib # Name for use in settings file --> name of module in "backends" directory. diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py index e58267a2e931..83dd46180484 100644 --- a/django/core/cache/backends/base.py +++ b/django/core/cache/backends/base.py @@ -1,10 +1,18 @@ "Base Cache class." -from django.core.exceptions import ImproperlyConfigured +import warnings + +from django.core.exceptions import ImproperlyConfigured, DjangoRuntimeWarning class InvalidCacheBackendError(ImproperlyConfigured): pass +class CacheKeyWarning(DjangoRuntimeWarning): + pass + +# Memcached does not accept keys longer than this. +MEMCACHE_MAX_KEY_LENGTH = 250 + class BaseCache(object): def __init__(self, params): timeout = params.get('timeout', 300) @@ -116,3 +124,21 @@ def delete_many(self, keys): def clear(self): """Remove *all* values from the cache at once.""" raise NotImplementedError + + def validate_key(self, key): + """ + Warn about keys that would not be portable to the memcached + backend. This encourages (but does not force) writing backend-portable + cache code. + + """ + if len(key) > MEMCACHE_MAX_KEY_LENGTH: + warnings.warn('Cache key will cause errors if used with memcached: ' + '%s (longer than %s)' % (key, MEMCACHE_MAX_KEY_LENGTH), + CacheKeyWarning) + for char in key: + if ord(char) < 33 or ord(char) == 127: + warnings.warn('Cache key contains characters that will cause ' + 'errors if used with memcached: %r' % key, + CacheKeyWarning) + diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index 3398e6a85b70..c4429c80b3b4 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -1,7 +1,7 @@ "Database cache backend." from django.core.cache.backends.base import BaseCache -from django.db import connection, transaction, DatabaseError +from django.db import connections, router, transaction, DatabaseError import base64, time from datetime import datetime try: @@ -9,10 +9,31 @@ except ImportError: import pickle +class Options(object): + """A class that will quack like a Django model _meta class. + + This allows cache operations to be controlled by the router + """ + def __init__(self, table): + self.db_table = table + self.app_label = 'django_cache' + self.module_name = 'cacheentry' + self.verbose_name = 'cache entry' + self.verbose_name_plural = 'cache entries' + self.object_name = 'CacheEntry' + self.abstract = False + self.managed = True + self.proxy = False + class CacheClass(BaseCache): def __init__(self, table, params): BaseCache.__init__(self, params) - self._table = connection.ops.quote_name(table) + self._table = table + + class CacheEntry(object): + _meta = Options(table) + self.cache_model_class = CacheEntry + max_entries = params.get('max_entries', 300) try: self._max_entries = int(max_entries) @@ -25,78 +46,100 @@ def __init__(self, table, params): self._cull_frequency = 3 def get(self, key, default=None): - cursor = connection.cursor() - cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % self._table, [key]) + self.validate_key(key) + db = router.db_for_read(self.cache_model_class) + table = connections[db].ops.quote_name(self._table) + cursor = connections[db].cursor() + + cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % table, [key]) row = cursor.fetchone() if row is None: return default now = datetime.now() if row[2] < now: - cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) - transaction.commit_unless_managed() + db = router.db_for_write(self.cache_model_class) + cursor = connections[db].cursor() + cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key]) + transaction.commit_unless_managed(using=db) return default - value = connection.ops.process_clob(row[1]) + value = connections[db].ops.process_clob(row[1]) return pickle.loads(base64.decodestring(value)) def set(self, key, value, timeout=None): + self.validate_key(key) self._base_set('set', key, value, timeout) def add(self, key, value, timeout=None): + 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: timeout = self.default_timeout - cursor = connection.cursor() - cursor.execute("SELECT COUNT(*) FROM %s" % self._table) + db = router.db_for_write(self.cache_model_class) + table = connections[db].ops.quote_name(self._table) + cursor = connections[db].cursor() + + cursor.execute("SELECT COUNT(*) FROM %s" % table) num = cursor.fetchone()[0] now = datetime.now().replace(microsecond=0) exp = datetime.fromtimestamp(time.time() + timeout).replace(microsecond=0) if num > self._max_entries: - self._cull(cursor, now) + self._cull(db, cursor, now) encoded = base64.encodestring(pickle.dumps(value, 2)).strip() - cursor.execute("SELECT cache_key, expires FROM %s WHERE cache_key = %%s" % self._table, [key]) + cursor.execute("SELECT cache_key, expires FROM %s WHERE cache_key = %%s" % table, [key]) try: result = cursor.fetchone() if result and (mode == 'set' or (mode == 'add' and result[1] < now)): - cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE cache_key = %%s" % self._table, - [encoded, connection.ops.value_to_db_datetime(exp), key]) + cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE cache_key = %%s" % table, + [encoded, connections[db].ops.value_to_db_datetime(exp), key]) else: - cursor.execute("INSERT INTO %s (cache_key, value, expires) VALUES (%%s, %%s, %%s)" % self._table, - [key, encoded, connection.ops.value_to_db_datetime(exp)]) + cursor.execute("INSERT INTO %s (cache_key, value, expires) VALUES (%%s, %%s, %%s)" % table, + [key, encoded, connections[db].ops.value_to_db_datetime(exp)]) except DatabaseError: # To be threadsafe, updates/inserts are allowed to fail silently - transaction.rollback_unless_managed() + transaction.rollback_unless_managed(using=db) return False else: - transaction.commit_unless_managed() + transaction.commit_unless_managed(using=db) return True def delete(self, key): - cursor = connection.cursor() - cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) - transaction.commit_unless_managed() + self.validate_key(key) + db = router.db_for_write(self.cache_model_class) + table = connections[db].ops.quote_name(self._table) + cursor = connections[db].cursor() + + cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key]) + transaction.commit_unless_managed(using=db) def has_key(self, key): + self.validate_key(key) + db = router.db_for_read(self.cache_model_class) + table = connections[db].ops.quote_name(self._table) + cursor = connections[db].cursor() + now = datetime.now().replace(microsecond=0) - cursor = connection.cursor() - cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s and expires > %%s" % self._table, - [key, connection.ops.value_to_db_datetime(now)]) + cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s and expires > %%s" % table, + [key, connections[db].ops.value_to_db_datetime(now)]) return cursor.fetchone() is not None - def _cull(self, cursor, now): + def _cull(self, db, cursor, now): if self._cull_frequency == 0: self.clear() else: - cursor.execute("DELETE FROM %s WHERE expires < %%s" % self._table, - [connection.ops.value_to_db_datetime(now)]) - cursor.execute("SELECT COUNT(*) FROM %s" % self._table) + table = connections[db].ops.quote_name(self._table) + cursor.execute("DELETE FROM %s WHERE expires < %%s" % table, + [connections[db].ops.value_to_db_datetime(now)]) + cursor.execute("SELECT COUNT(*) FROM %s" % table) num = cursor.fetchone()[0] if num > self._max_entries: - cursor.execute("SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" % self._table, [num / self._cull_frequency]) - cursor.execute("DELETE FROM %s WHERE cache_key < %%s" % self._table, [cursor.fetchone()[0]]) + cursor.execute("SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" % table, [num / self._cull_frequency]) + cursor.execute("DELETE FROM %s WHERE cache_key < %%s" % table, [cursor.fetchone()[0]]) def clear(self): - cursor = connection.cursor() - cursor.execute('DELETE FROM %s' % self._table) + db = router.db_for_write(self.cache_model_class) + table = connections[db].ops.quote_name(self._table) + cursor = connections[db].cursor() + cursor.execute('DELETE FROM %s' % table) diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py index 4337484cb11f..f73b7408bc16 100644 --- a/django/core/cache/backends/dummy.py +++ b/django/core/cache/backends/dummy.py @@ -6,22 +6,25 @@ class CacheClass(BaseCache): def __init__(self, *args, **kwargs): pass - def add(self, *args, **kwargs): + def add(self, key, *args, **kwargs): + self.validate_key(key) return True def get(self, key, default=None): + self.validate_key(key) return default - def set(self, *args, **kwargs): - pass + def set(self, key, *args, **kwargs): + self.validate_key(key) - def delete(self, *args, **kwargs): - pass + def delete(self, key, *args, **kwargs): + self.validate_key(key) def get_many(self, *args, **kwargs): return {} - def has_key(self, *args, **kwargs): + def has_key(self, key, *args, **kwargs): + self.validate_key(key) return False def set_many(self, *args, **kwargs): diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py index fe833336d0d9..46e69f309174 100644 --- a/django/core/cache/backends/filebased.py +++ b/django/core/cache/backends/filebased.py @@ -32,6 +32,7 @@ def __init__(self, dir, params): self._createdir() def add(self, key, value, timeout=None): + self.validate_key(key) if self.has_key(key): return False @@ -39,6 +40,7 @@ def add(self, key, value, timeout=None): return True def get(self, key, default=None): + self.validate_key(key) fname = self._key_to_file(key) try: f = open(fname, 'rb') @@ -56,6 +58,7 @@ def get(self, key, default=None): return default def set(self, key, value, timeout=None): + self.validate_key(key) fname = self._key_to_file(key) dirname = os.path.dirname(fname) @@ -79,6 +82,7 @@ def set(self, key, value, timeout=None): pass def delete(self, key): + self.validate_key(key) try: self._delete(self._key_to_file(key)) except (IOError, OSError): @@ -95,6 +99,7 @@ def _delete(self, fname): pass def has_key(self, key): + self.validate_key(key) fname = self._key_to_file(key) try: f = open(fname, 'rb') @@ -116,7 +121,7 @@ def _cull(self): return try: - filelist = os.listdir(self._dir) + filelist = sorted(os.listdir(self._dir)) except (IOError, OSError): return diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py index eff1201b9777..fe33d333070e 100644 --- a/django/core/cache/backends/locmem.py +++ b/django/core/cache/backends/locmem.py @@ -30,6 +30,7 @@ def __init__(self, _, params): self._lock = RWLock() def add(self, key, value, timeout=None): + self.validate_key(key) self._lock.writer_enters() try: exp = self._expire_info.get(key) @@ -44,6 +45,7 @@ def add(self, key, value, timeout=None): self._lock.writer_leaves() def get(self, key, default=None): + self.validate_key(key) self._lock.reader_enters() try: exp = self._expire_info.get(key) @@ -76,6 +78,7 @@ def _set(self, key, value, timeout=None): self._expire_info[key] = time.time() + timeout def set(self, key, value, timeout=None): + self.validate_key(key) self._lock.writer_enters() # Python 2.4 doesn't allow combined try-except-finally blocks. try: @@ -87,6 +90,7 @@ def set(self, key, value, timeout=None): self._lock.writer_leaves() def has_key(self, key): + self.validate_key(key) self._lock.reader_enters() try: exp = self._expire_info.get(key) @@ -127,6 +131,7 @@ def _delete(self, key): pass def delete(self, key): + self.validate_key(key) self._lock.writer_enters() try: self._delete(key) diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index 7d6b5b362b7c..5e2eaa72a979 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -40,8 +40,6 @@ def _get_memcache_timeout(self, timeout): return timeout def add(self, key, value, timeout=0): - if isinstance(value, unicode): - value = value.encode('utf-8') return self._cache.add(smart_str(key), value, self._get_memcache_timeout(timeout)) def get(self, key, default=None): @@ -92,8 +90,6 @@ def decr(self, key, delta=1): def set_many(self, data, timeout=0): safe_data = {} for key, value in data.items(): - if isinstance(value, unicode): - value = value.encode('utf-8') safe_data[smart_str(key)] = value self._cache.set_multi(safe_data, self._get_memcache_timeout(timeout)) diff --git a/django/core/exceptions.py b/django/core/exceptions.py index ee6d5fe37b11..21be8702fa49 100644 --- a/django/core/exceptions.py +++ b/django/core/exceptions.py @@ -1,4 +1,9 @@ -"Global Django exceptions" +""" +Global Django exception and warning classes. +""" + +class DjangoRuntimeWarning(RuntimeWarning): + pass class ObjectDoesNotExist(Exception): "The requested object does not exist" diff --git a/django/core/files/images.py b/django/core/files/images.py index 55008b548a9a..228a7118c51a 100644 --- a/django/core/files/images.py +++ b/django/core/files/images.py @@ -23,23 +23,26 @@ def _get_image_dimensions(self): if not hasattr(self, '_dimensions_cache'): close = self.closed self.open() - self._dimensions_cache = get_image_dimensions(self) - if close: - self.close() + self._dimensions_cache = get_image_dimensions(self, close=close) return self._dimensions_cache -def get_image_dimensions(file_or_path): - """Returns the (width, height) of an image, given an open file or a path.""" +def get_image_dimensions(file_or_path, close=False): + """ + Returns the (width, height) of an image, given an open file or a path. Set + 'close' to True to close the file at the end if it is initially in an open + state. + """ # Try to import PIL in either of the two ways it can end up installed. try: from PIL import ImageFile as PILImageFile except ImportError: import ImageFile as PILImageFile - + p = PILImageFile.Parser() - close = False if hasattr(file_or_path, 'read'): file = file_or_path + file_pos = file.tell() + file.seek(0) else: file = open(file_or_path, 'rb') close = True @@ -55,3 +58,5 @@ def get_image_dimensions(file_or_path): finally: if close: file.close() + else: + file.seek(file_pos) diff --git a/django/core/files/storage.py b/django/core/files/storage.py index 4f27502167d1..40480e33e167 100644 --- a/django/core/files/storage.py +++ b/django/core/files/storage.py @@ -7,7 +7,7 @@ from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.core.files import locks, File from django.core.files.move import file_move_safe -from django.utils.encoding import force_unicode +from django.utils.encoding import force_unicode, filepath_to_uri from django.utils.functional import LazyObject from django.utils.importlib import import_module from django.utils.text import get_valid_filename @@ -116,7 +116,7 @@ def size(self, name): def url(self, name): """ Returns an absolute URL where the file's contents can be accessed - directly by a web browser. + directly by a Web browser. """ raise NotImplementedError() @@ -218,7 +218,7 @@ def size(self, name): def url(self, name): if self.base_url is None: raise ValueError("This file is not accessible via a URL.") - return urlparse.urljoin(self.base_url, name).replace('\\', '/') + return urlparse.urljoin(self.base_url, filepath_to_uri(name)) def get_storage_class(import_path=None): if import_path is None: diff --git a/django/core/files/uploadhandler.py b/django/core/files/uploadhandler.py old mode 100755 new mode 100644 index 6c769780e501..2afb79e0513e --- a/django/core/files/uploadhandler.py +++ b/django/core/files/uploadhandler.py @@ -109,7 +109,7 @@ def file_complete(self, file_size): Signal that a file has completed. File size corresponds to the actual size accumulated by all the chunks. - Subclasses must should return a valid ``UploadedFile`` object. + Subclasses should return a valid ``UploadedFile`` object. """ raise NotImplementedError() diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index b03c2fd71e77..45f8445f0b1c 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -208,7 +208,7 @@ def get_script_name(environ): # If Apache's mod_rewrite had a whack at the URL, Apache set either # SCRIPT_URL or REDIRECT_URL to the full resource URL before applying any - # rewrites. Unfortunately not every webserver (lighttpd!) passes this + # rewrites. Unfortunately not every Web server (lighttpd!) passes this # information through all the time, so FORCE_SCRIPT_NAME, above, is still # needed. script_url = environ.get('SCRIPT_URL', u'') diff --git a/django/core/handlers/wsgi.py b/django/core/handlers/wsgi.py index 927b0988159c..b22d30a565d8 100644 --- a/django/core/handlers/wsgi.py +++ b/django/core/handlers/wsgi.py @@ -225,10 +225,17 @@ def __call__(self, environ, start_response): # settings weren't available. if self._request_middleware is None: self.initLock.acquire() - # Check that middleware is still uninitialised. - if self._request_middleware is None: - self.load_middleware() - self.initLock.release() + try: + try: + # Check that middleware is still uninitialised. + if self._request_middleware is None: + self.load_middleware() + except: + # Unload whatever middleware we got + self._request_middleware = None + raise + finally: + self.initLock.release() set_script_prefix(base.get_script_name(environ)) signals.request_started.send(sender=self.__class__) diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py index f9d1210791a6..8a2d9bf096f9 100644 --- a/django/core/mail/__init__.py +++ b/django/core/mail/__init__.py @@ -87,7 +87,7 @@ def mail_admins(subject, message, fail_silently=False, connection=None): """Sends a message to the admins, as defined by the ADMINS setting.""" if not settings.ADMINS: return - EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, + EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS], connection=connection).send(fail_silently=fail_silently) @@ -96,7 +96,7 @@ def mail_managers(subject, message, fail_silently=False, connection=None): """Sends a message to the managers, as defined by the MANAGERS setting.""" if not settings.MANAGERS: return - EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, + EmailMessage(u'%s%s' % (settings.EMAIL_SUBJECT_PREFIX, subject), message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS], connection=connection).send(fail_silently=fail_silently) diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index 63efe438d3f8..bb184ab31250 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -1,5 +1,4 @@ """SMTP email backend class.""" - import smtplib import socket import threading @@ -7,6 +6,8 @@ from django.conf import settings from django.core.mail.backends.base import BaseEmailBackend from django.core.mail.utils import DNS_NAME +from django.core.mail.message import sanitize_address + class EmailBackend(BaseEmailBackend): """ @@ -95,9 +96,11 @@ def _send(self, email_message): """A helper method that does the actual sending.""" if not email_message.recipients(): return False + from_email = sanitize_address(email_message.from_email, email_message.encoding) + recipients = [sanitize_address(addr, email_message.encoding) + for addr in email_message.recipients()] try: - self.connection.sendmail(email_message.from_email, - email_message.recipients(), + self.connection.sendmail(from_email, recipients, email_message.message().as_string()) except: if not self.fail_silently: diff --git a/django/core/mail/message.py b/django/core/mail/message.py index 91a10a0a5cd3..50d5a5454c05 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -3,16 +3,25 @@ import random import time from email import Charset, Encoders +try: + from email.generator import Generator +except ImportError: + from email.Generator import Generator # TODO: Remove when remove Python 2.4 support from email.MIMEText import MIMEText from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.Header import Header -from email.Utils import formatdate, getaddresses, formataddr +from email.Utils import formatdate, getaddresses, formataddr, parseaddr from django.conf import settings from django.core.mail.utils import DNS_NAME from django.utils.encoding import smart_str, force_unicode +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from # some spam filters. Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') @@ -54,6 +63,22 @@ def make_msgid(idstring=None): return msgid +# Header names that contain structured address data (RFC #5322) +ADDRESS_HEADERS = set([ + 'from', + 'sender', + 'reply-to', + 'to', + 'cc', + 'bcc', + 'resent-from', + 'resent-sender', + 'resent-to', + 'resent-cc', + 'resent-bcc', +]) + + def forbid_multi_line_headers(name, val, encoding): """Forbids multi-line headers, to prevent header injection.""" encoding = encoding or settings.DEFAULT_CHARSET @@ -63,39 +88,83 @@ def forbid_multi_line_headers(name, val, encoding): try: val = val.encode('ascii') except UnicodeEncodeError: - if name.lower() in ('to', 'from', 'cc'): - result = [] - for nm, addr in getaddresses((val,)): - nm = str(Header(nm.encode(encoding), encoding)) - result.append(formataddr((nm, str(addr)))) - val = ', '.join(result) + if name.lower() in ADDRESS_HEADERS: + val = ', '.join(sanitize_address(addr, encoding) + for addr in getaddresses((val,))) else: - val = Header(val.encode(encoding), encoding) + val = str(Header(val, encoding)) else: if name.lower() == 'subject': val = Header(val) return name, val + +def sanitize_address(addr, encoding): + if isinstance(addr, basestring): + addr = parseaddr(force_unicode(addr)) + nm, addr = addr + nm = str(Header(nm, encoding)) + try: + addr = addr.encode('ascii') + except UnicodeEncodeError: # IDN + if u'@' in addr: + localpart, domain = addr.split(u'@', 1) + localpart = str(Header(localpart, encoding)) + domain = domain.encode('idna') + addr = '@'.join([localpart, domain]) + else: + addr = str(Header(addr, encoding)) + return formataddr((nm, addr)) + + class SafeMIMEText(MIMEText): - + def __init__(self, text, subtype, charset): self.encoding = charset MIMEText.__init__(self, text, subtype, charset) - - def __setitem__(self, name, val): + + def __setitem__(self, name, val): name, val = forbid_multi_line_headers(name, val, self.encoding) MIMEText.__setitem__(self, name, val) + def as_string(self, unixfrom=False): + """Return the entire formatted message as a string. + Optional `unixfrom' when True, means include the Unix From_ envelope + header. + + This overrides the default as_string() implementation to not mangle + lines that begin with 'From '. See bug #13433 for details. + """ + fp = StringIO() + g = Generator(fp, mangle_from_ = False) + g.flatten(self, unixfrom=unixfrom) + return fp.getvalue() + + class SafeMIMEMultipart(MIMEMultipart): - + def __init__(self, _subtype='mixed', boundary=None, _subparts=None, encoding=None, **_params): self.encoding = encoding MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params) - + def __setitem__(self, name, val): name, val = forbid_multi_line_headers(name, val, self.encoding) MIMEMultipart.__setitem__(self, name, val) + def as_string(self, unixfrom=False): + """Return the entire formatted message as a string. + Optional `unixfrom' when True, means include the Unix From_ envelope + header. + + This overrides the default as_string() implementation to not mangle + lines that begin with 'From '. See bug #13433 for details. + """ + fp = StringIO() + g = Generator(fp, mangle_from_ = False) + g.flatten(self, unixfrom=unixfrom) + return fp.getvalue() + + class EmailMessage(object): """ A container for email information. @@ -262,7 +331,7 @@ def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, conversions. """ super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers) - self.alternatives=alternatives or [] + self.alternatives = alternatives or [] def attach_alternative(self, content, mimetype): """Attach an alternative content representation.""" diff --git a/django/core/management/__init__.py b/django/core/management/__init__.py index 32e744374a7f..bafab17112ad 100644 --- a/django/core/management/__init__.py +++ b/django/core/management/__init__.py @@ -250,15 +250,15 @@ def fetch_command(self, subcommand): """ try: app_name = get_commands()[subcommand] - if isinstance(app_name, BaseCommand): - # If the command is already loaded, use it directly. - klass = app_name - else: - klass = load_command_class(app_name, subcommand) except KeyError: sys.stderr.write("Unknown command: %r\nType '%s help' for usage.\n" % \ (subcommand, self.prog_name)) sys.exit(1) + if isinstance(app_name, BaseCommand): + # If the command is already loaded, use it directly. + klass = app_name + else: + klass = load_command_class(app_name, subcommand) return klass def autocomplete(self): @@ -372,7 +372,7 @@ def execute(self): elif self.argv[1:] == ['--version']: # LaxOptionParser already takes care of printing the version. pass - elif self.argv[1:] == ['--help']: + elif self.argv[1:] in (['--help'], ['-h']): parser.print_lax_help() sys.stderr.write(self.main_help_text() + '\n') else: diff --git a/django/core/management/base.py b/django/core/management/base.py index 6761cb69bc76..282fbd7fb4cf 100644 --- a/django/core/management/base.py +++ b/django/core/management/base.py @@ -225,10 +225,10 @@ def execute(self, *args, **options): from django.db import connections, DEFAULT_DB_ALIAS connection = connections[options.get('database', DEFAULT_DB_ALIAS)] if connection.ops.start_transaction_sql(): - self.stdout.write(self.style.SQL_KEYWORD(connection.ops.start_transaction_sql())) + self.stdout.write(self.style.SQL_KEYWORD(connection.ops.start_transaction_sql()) + '\n') self.stdout.write(output) if self.output_transaction: - self.stdout.write(self.style.SQL_KEYWORD("COMMIT;") + '\n') + self.stdout.write('\n' + self.style.SQL_KEYWORD("COMMIT;") + '\n') except CommandError, e: self.stderr.write(smart_str(self.style.ERROR('Error: %s\n' % e))) sys.exit(1) @@ -394,9 +394,9 @@ def copy_helper(style, app_or_project, name, directory, other_name=''): relative_dir = d[len(template_dir)+1:].replace('%s_name' % app_or_project, name) if relative_dir: os.mkdir(os.path.join(top_dir, relative_dir)) - for i, subdir in enumerate(subdirs): + for subdir in subdirs[:]: if subdir.startswith('.'): - del subdirs[i] + subdirs.remove(subdir) for f in files: if not f.endswith('.py'): # Ignore .pyc, .pyo, .py.class etc, as they cause various diff --git a/django/core/management/commands/compilemessages.py b/django/core/management/commands/compilemessages.py index 7d600e0fe985..b5eaeb1f5263 100644 --- a/django/core/management/commands/compilemessages.py +++ b/django/core/management/commands/compilemessages.py @@ -1,9 +1,17 @@ +import codecs import os import sys from optparse import make_option from django.core.management.base import BaseCommand, CommandError -def compile_messages(locale=None): +def has_bom(fn): + f = open(fn, 'r') + sample = f.read(4) + return sample[:3] == '\xef\xbb\xbf' or \ + sample.startswith(codecs.BOM_UTF16_LE) or \ + sample.startswith(codecs.BOM_UTF16_BE) + +def compile_messages(stderr, locale=None): basedirs = [os.path.join('conf', 'locale'), 'locale'] if os.environ.get('DJANGO_SETTINGS_MODULE'): from django.conf import settings @@ -21,8 +29,11 @@ def compile_messages(locale=None): for dirpath, dirnames, filenames in os.walk(basedir): for f in filenames: if f.endswith('.po'): - sys.stderr.write('processing file %s in %s\n' % (f, dirpath)) - pf = os.path.splitext(os.path.join(dirpath, f))[0] + stderr.write('processing file %s in %s\n' % (f, dirpath)) + fn = os.path.join(dirpath, f) + if has_bom(fn): + raise CommandError("The %s file has a BOM (Byte Order Mark). Django only supports .po files encoded in UTF-8 and without any BOM." % fn) + pf = os.path.splitext(fn)[0] # Store the names of the .mo and .po files in an environment # variable, rather than doing a string replacement into the # command, so that we can take advantage of shell quoting, to @@ -49,4 +60,4 @@ class Command(BaseCommand): def handle(self, **options): locale = options.get('locale') - compile_messages(locale) + compile_messages(self.stderr, locale=locale) diff --git a/django/core/management/commands/dumpdata.py b/django/core/management/commands/dumpdata.py index aaaa5845a572..23c03e7b17fa 100644 --- a/django/core/management/commands/dumpdata.py +++ b/django/core/management/commands/dumpdata.py @@ -20,7 +20,8 @@ class Command(BaseCommand): make_option('-n', '--natural', action='store_true', dest='use_natural_keys', default=False, help='Use natural keys if they are available.'), ) - help = 'Output the contents of the database as a fixture of the given format.' + help = ("Output the contents of the database as a fixture of the given " + "format (using each model's default manager).") args = '[appname appname.ModelName ...]' def handle(self, *app_labels, **options): @@ -163,4 +164,4 @@ def sort_dependencies(app_list): ) model_dependencies = skipped - return model_list \ No newline at end of file + return model_list diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 6836fe35cab8..f7e769bdac57 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -1,7 +1,7 @@ from optparse import make_option from django.conf import settings -from django.db import connections, transaction, models, DEFAULT_DB_ALIAS +from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS from django.core.management import call_command from django.core.management.base import NoArgsCommand, CommandError from django.core.management.color import no_style @@ -66,7 +66,13 @@ def handle_noargs(self, **options): # Emit the post sync signal. This allows individual # applications to respond as if the database had been # sync'd from scratch. - emit_post_sync_signal(models.get_models(), verbosity, interactive, db) + all_models = [] + for app in models.get_apps(): + all_models.extend([ + m for m in models.get_models(app, include_auto_created=True) + if router.allow_syncdb(db, m) + ]) + emit_post_sync_signal(all_models, verbosity, interactive, db) # Reinstall the initial_data fixture. kwargs = options.copy() diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py index e45f22c28762..5f0e278c61ab 100644 --- a/django/core/management/commands/inspectdb.py +++ b/django/core/management/commands/inspectdb.py @@ -20,7 +20,7 @@ class Command(NoArgsCommand): def handle_noargs(self, **options): try: for line in self.handle_inspection(options): - print line + self.stdout.write("%s\n" % line) except NotImplementedError: raise CommandError("Database inspection isn't supported for the currently selected database backend.") @@ -66,12 +66,11 @@ def handle_inspection(self, options): if ' ' in att_name: att_name = att_name.replace(' ', '_') comment_notes.append('Field renamed to remove spaces.') + if '-' in att_name: att_name = att_name.replace('-', '_') comment_notes.append('Field renamed to remove dashes.') - if keyword.iskeyword(att_name): - att_name += '_field' - comment_notes.append('Field renamed because it was a Python reserved word.') + if column_name != att_name: comment_notes.append('Field name made lowercase.') @@ -97,6 +96,10 @@ def handle_inspection(self, options): extra_params['unique'] = True field_type += '(' + + if keyword.iskeyword(att_name): + att_name += '_field' + comment_notes.append('Field renamed because it was a Python reserved word.') # Don't output 'id = meta.AutoField(primary_key=True)', because # that's assumed if it doesn't exist. diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index caf3b11b85a9..b8bb62feca6e 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -47,7 +47,8 @@ def handle(self, *fixture_labels, **options): # Keep a count of the installed objects and fixtures fixture_count = 0 - object_count = 0 + loaded_object_count = 0 + fixture_object_count = 0 models = set() humanize = lambda dirname: dirname and "'%s'" % dirname or 'absolute path' @@ -114,11 +115,12 @@ def read(self): if verbosity > 1: self.stdout.write("Loading '%s' fixtures...\n" % fixture_name) else: - sys.stderr.write( + self.stderr.write( self.style.ERROR("Problem installing fixture '%s': %s is not a known serialization format.\n" % (fixture_name, format))) - transaction.rollback(using=using) - transaction.leave_transaction_management(using=using) + if commit: + transaction.rollback(using=using) + transaction.leave_transaction_management(using=using) return if os.path.isabs(fixture_name): @@ -151,35 +153,40 @@ def read(self): fixture.close() self.stderr.write(self.style.ERROR("Multiple fixtures named '%s' in %s. Aborting.\n" % (fixture_name, humanize(fixture_dir)))) - transaction.rollback(using=using) - transaction.leave_transaction_management(using=using) + if commit: + transaction.rollback(using=using) + transaction.leave_transaction_management(using=using) return else: fixture_count += 1 objects_in_fixture = 0 + loaded_objects_in_fixture = 0 if verbosity > 0: self.stdout.write("Installing %s fixture '%s' from %s.\n" % \ (format, fixture_name, humanize(fixture_dir))) try: objects = serializers.deserialize(format, fixture, using=using) for obj in objects: + objects_in_fixture += 1 if router.allow_syncdb(using, obj.object.__class__): - objects_in_fixture += 1 + loaded_objects_in_fixture += 1 models.add(obj.object.__class__) obj.save(using=using) - object_count += objects_in_fixture + loaded_object_count += loaded_objects_in_fixture + fixture_object_count += objects_in_fixture label_found = True except (SystemExit, KeyboardInterrupt): raise except Exception: import traceback fixture.close() - transaction.rollback(using=using) - transaction.leave_transaction_management(using=using) + if commit: + transaction.rollback(using=using) + transaction.leave_transaction_management(using=using) if show_traceback: traceback.print_exc() else: - sys.stderr.write( + self.stderr.write( self.style.ERROR("Problem installing fixture '%s': %s\n" % (full_path, ''.join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback))))) @@ -189,11 +196,12 @@ def read(self): # If the fixture we loaded contains 0 objects, assume that an # error was encountered during fixture loading. if objects_in_fixture == 0: - sys.stderr.write( + self.stderr.write( self.style.ERROR("No fixture data found for '%s'. (File format may be invalid.)\n" % (fixture_name))) - transaction.rollback(using=using) - transaction.leave_transaction_management(using=using) + if commit: + transaction.rollback(using=using) + transaction.leave_transaction_management(using=using) return except Exception, e: @@ -203,7 +211,7 @@ def read(self): # If we found even one object in a fixture, we need to reset the # database sequences. - if object_count > 0: + if loaded_object_count > 0: sequence_sql = connection.ops.sequence_reset_sql(self.style, models) if sequence_sql: if verbosity > 1: @@ -215,12 +223,17 @@ def read(self): transaction.commit(using=using) transaction.leave_transaction_management(using=using) - if object_count == 0: + if fixture_object_count == 0: if verbosity > 0: self.stdout.write("No fixtures found.\n") else: if verbosity > 0: - self.stdout.write("Installed %d object(s) from %d fixture(s)\n" % (object_count, fixture_count)) + if fixture_object_count == loaded_object_count: + self.stdout.write("Installed %d object(s) from %d fixture(s)\n" % ( + loaded_object_count, fixture_count)) + else: + self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)\n" % ( + loaded_object_count, fixture_object_count, fixture_count)) # Close the DB connection. This is required as a workaround for an # edge case in MySQL: if the same connection is used to diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 96169020e5c4..abb440e69a7b 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -23,11 +23,21 @@ def handle_noargs(self, **options): if use_plain: # Don't bother loading IPython, because the user wants plain Python. raise ImportError - import IPython - # Explicitly pass an empty list as arguments, because otherwise IPython - # would use sys.argv from this script. - shell = IPython.Shell.IPShell(argv=[]) - shell.mainloop() + try: + from IPython.frontend.terminal.embed import TerminalInteractiveShell + shell = TerminalInteractiveShell() + shell.mainloop() + except ImportError: + # IPython < 0.11 + # Explicitly pass an empty list as arguments, because otherwise + # IPython would use sys.argv from this script. + try: + from IPython.Shell import IPShell + shell = IPShell(argv=[]) + shell.mainloop() + except ImportError: + # IPython not found at all, raise ImportError + raise except ImportError: import code # Set up a dictionary to serve as the environment for the shell, so diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index d1dd49b75e65..ca177c646dc1 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -26,6 +26,10 @@ def handle_noargs(self, **options): interactive = options.get('interactive') show_traceback = options.get('traceback', False) + # Stealth option -- 'load_initial_data' is used by the testing setup + # process to disable initial fixture loading. + load_initial_data = options.get('load_initial_data', True) + self.style = no_style() # Import the 'management' module within each installed app, to register @@ -148,5 +152,7 @@ def model_installed(model): else: transaction.commit_unless_managed(using=db) - from django.core.management import call_command - call_command('loaddata', 'initial_data', verbosity=verbosity, database=db) + # Load initial_data fixtures (unless that has been disabled) + if load_initial_data: + from django.core.management import call_command + call_command('loaddata', 'initial_data', verbosity=verbosity, database=db) diff --git a/django/core/management/sql.py b/django/core/management/sql.py index 695473dfa5d6..86d91fa8b787 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -2,12 +2,9 @@ import re from django.conf import settings -from django.contrib.contenttypes import generic from django.core.management.base import CommandError -from django.dispatch import dispatcher from django.db import models from django.db.models import get_models -from django.db.backends.util import truncate_name def sql_create(app, style, connection): "Returns a list of the CREATE TABLE SQL statements for the given app." @@ -18,7 +15,7 @@ def sql_create(app, style, connection): raise CommandError("Django doesn't know which syntax to use for your SQL statements,\n" + "because you haven't specified the ENGINE setting for the database.\n" + "Edit your settings file and change DATBASES['default']['ENGINE'] to something like\n" + - "'django.db.backends.postgresql' or 'django.db.backends.mysql'") + "'django.db.backends.postgresql' or 'django.db.backends.mysql'.") # Get installed models, so we generate REFERENCES right. # We trim models from the current app so that the sqlreset command does not diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 047acb049535..d1b272ddd1d7 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -1,7 +1,14 @@ import sys + +from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation from django.core.management.color import color_style from django.utils.itercompat import is_iterable +try: + any +except NameError: + from django.utils.itercompat import any + class ModelErrorCollection: def __init__(self, outfile=sys.stdout): self.errors = [] @@ -45,11 +52,14 @@ def get_validation_errors(outfile, app=None): except (ValueError, TypeError): e.add(opts, '"%s": CharFields require a "max_length" attribute that is a positive integer.' % f.name) if isinstance(f, models.DecimalField): + decimalp_ok, mdigits_ok = False, False decimalp_msg ='"%s": DecimalFields require a "decimal_places" attribute that is a non-negative integer.' try: decimal_places = int(f.decimal_places) if decimal_places < 0: e.add(opts, decimalp_msg % f.name) + else: + decimalp_ok = True except (ValueError, TypeError): e.add(opts, decimalp_msg % f.name) mdigits_msg = '"%s": DecimalFields require a "max_digits" attribute that is a positive integer.' @@ -57,8 +67,14 @@ def get_validation_errors(outfile, app=None): max_digits = int(f.max_digits) if max_digits <= 0: e.add(opts, mdigits_msg % f.name) + else: + mdigits_ok = True except (ValueError, TypeError): e.add(opts, mdigits_msg % f.name) + invalid_values_msg = '"%s": DecimalFields require a "max_digits" attribute value that is greater than the value of the "decimal_places" attribute.' + if decimalp_ok and mdigits_ok: + if decimal_places >= max_digits: + e.add(opts, invalid_values_msg % f.name) if isinstance(f, models.FileField) and not f.upload_to: e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name) if isinstance(f, models.ImageField): @@ -216,6 +232,12 @@ def get_validation_errors(outfile, app=None): e.add(opts, "'%s' specifies an m2m relation through model %s, " "which has not been installed" % (f.name, f.rel.through) ) + elif isinstance(f, GenericRelation): + if not any([isinstance(vfield, GenericForeignKey) for vfield in f.rel.to._meta.virtual_fields]): + e.add(opts, "Model '%s' must have a GenericForeignKey in " + "order to create a GenericRelation that points to it." + % f.rel.to.__name__ + ) rel_opts = f.rel.to._meta rel_name = RelatedObject(f.rel.to, cls, f).get_accessor_name() @@ -257,7 +279,7 @@ def get_validation_errors(outfile, app=None): continue # Skip ordering in the format field1__field2 (FIXME: checking # this format would be nice, but it's a little fiddly). - if '_' in field_name: + if '__' in field_name: continue try: opts.get_field(field_name, many_to_many=False) diff --git a/django/core/serializers/__init__.py b/django/core/serializers/__init__.py index 32f135009c03..7afc94c1d057 100644 --- a/django/core/serializers/__init__.py +++ b/django/core/serializers/__init__.py @@ -36,7 +36,7 @@ _serializers = {} def register_serializer(format, serializer_module, serializers=None): - """"Register a new serializer. + """Register a new serializer. ``serializer_module`` should be the fully qualified module name for the serializer. @@ -48,6 +48,8 @@ def register_serializer(format, serializer_module, serializers=None): directly into the global register of serializers. Adding serializers directly is not a thread-safe operation. """ + if serializers is None and not _serializers: + _load_serializers() module = importlib.import_module(serializer_module) if serializers is None: _serializers[format] = module @@ -56,6 +58,8 @@ def register_serializer(format, serializer_module, serializers=None): def unregister_serializer(format): "Unregister a given serializer. This is not a thread-safe operation." + if not _serializers: + _load_serializers() del _serializers[format] def get_serializer(format): diff --git a/django/core/serializers/base.py b/django/core/serializers/base.py index 190636e67058..cdcc7fa36aef 100644 --- a/django/core/serializers/base.py +++ b/django/core/serializers/base.py @@ -31,9 +31,9 @@ def serialize(self, queryset, **options): """ self.options = options - self.stream = options.get("stream", StringIO()) - self.selected_fields = options.get("fields") - self.use_natural_keys = options.get("use_natural_keys", False) + self.stream = options.pop("stream", StringIO()) + self.selected_fields = options.pop("fields", None) + self.use_natural_keys = options.pop("use_natural_keys", False) self.start_serialization() for obj in queryset: diff --git a/django/core/serializers/json.py b/django/core/serializers/json.py index b82c0a0ec71e..b8119f54d43f 100644 --- a/django/core/serializers/json.py +++ b/django/core/serializers/json.py @@ -18,9 +18,6 @@ class Serializer(PythonSerializer): internal_use_only = False def end_serialization(self): - self.options.pop('stream', None) - self.options.pop('fields', None) - self.options.pop('use_natural_keys', None) simplejson.dump(self.objects, self.stream, cls=DjangoJSONEncoder, **self.options) def getvalue(self): diff --git a/django/core/serializers/pyyaml.py b/django/core/serializers/pyyaml.py index 2ca68fe44268..c443b63bc94f 100644 --- a/django/core/serializers/pyyaml.py +++ b/django/core/serializers/pyyaml.py @@ -38,9 +38,6 @@ def handle_field(self, obj, field): super(Serializer, self).handle_field(obj, field) def end_serialization(self): - self.options.pop('stream', None) - self.options.pop('fields', None) - self.options.pop('use_natural_keys', None) yaml.dump(self.objects, self.stream, Dumper=DjangoSafeDumper, **self.options) def getvalue(self): diff --git a/django/core/serializers/xml_serializer.py b/django/core/serializers/xml_serializer.py index 5fef3b6936af..bcf5631e00f1 100644 --- a/django/core/serializers/xml_serializer.py +++ b/django/core/serializers/xml_serializer.py @@ -42,10 +42,16 @@ def start_object(self, obj): raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj)) self.indent(1) - self.xml.startElement("object", { - "pk" : smart_unicode(obj._get_pk_val()), - "model" : smart_unicode(obj._meta), - }) + obj_pk = obj._get_pk_val() + if obj_pk is None: + attrs = {"model": smart_unicode(obj._meta),} + else: + attrs = { + "pk": smart_unicode(obj._get_pk_val()), + "model": smart_unicode(obj._meta), + } + + self.xml.startElement("object", attrs) def end_object(self, obj): """ @@ -166,11 +172,12 @@ def _handle_object(self, node): # bail. Model = self._get_model_from_node(node, "model") - # Start building a data dictionary from the object. If the node is - # missing the pk attribute, bail. - pk = node.getAttribute("pk") - if not pk: - raise base.DeserializationError(" node is missing the 'pk' attribute") + # Start building a data dictionary from the object. + # If the node is missing the pk set it to None + if node.hasAttribute("pk"): + pk = node.getAttribute("pk") + else: + pk = None data = {Model._meta.pk.attname : Model._meta.pk.to_python(pk)} diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py index 607cdb4628ac..7e724c251067 100644 --- a/django/core/servers/fastcgi.py +++ b/django/core/servers/fastcgi.py @@ -27,26 +27,26 @@ Optional Fcgi settings: (setting=value) protocol=PROTOCOL fcgi, scgi, ajp, ... (default fcgi) - host=HOSTNAME hostname to listen on.. + host=HOSTNAME hostname to listen on. port=PORTNUM port to listen on. socket=FILE UNIX socket to listen on. - method=IMPL prefork or threaded (default prefork) - maxrequests=NUMBER number of requests a child handles before it is + method=IMPL prefork or threaded (default prefork). + maxrequests=NUMBER number of requests a child handles before it is killed and a new child is forked (0 = no limit). - maxspare=NUMBER max number of spare processes / threads + maxspare=NUMBER max number of spare processes / threads. minspare=NUMBER min number of spare processes / threads. - maxchildren=NUMBER hard limit number of processes / threads + maxchildren=NUMBER hard limit number of processes / threads. daemonize=BOOL whether to detach from terminal. pidfile=FILE write the spawned process-id to this file. workdir=DIRECTORY change to this directory when daemonizing. - debug=BOOL set to true to enable flup tracebacks + debug=BOOL set to true to enable flup tracebacks. outlog=FILE write stdout to this file. errlog=FILE write stderr to this file. - umask=UMASK umask to use when daemonizing (default 022). + umask=UMASK umask to use when daemonizing, in octal notation (default 022). Examples: Run a "standard" fastcgi process on a file-descriptor - (for webservers which spawn your processes for you) + (for Web servers which spawn your processes for you) $ manage.py runfcgi method=threaded Run a scgi server on a TCP host/port @@ -166,7 +166,7 @@ def runfastcgi(argset=[], **kwargs): if options['errlog']: daemon_kwargs['err_log'] = options['errlog'] if options['umask']: - daemon_kwargs['umask'] = int(options['umask']) + daemon_kwargs['umask'] = int(options['umask'], 8) if daemonize: from django.utils.daemonize import become_daemon diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index cad57a5d9ff1..8ecec941fc14 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -183,7 +183,8 @@ def _populate(self): else: bits = normalize(p_pattern) lookups.appendlist(pattern.callback, (bits, p_pattern)) - lookups.appendlist(pattern.name, (bits, p_pattern)) + if pattern.name is not None: + lookups.appendlist(pattern.name, (bits, p_pattern)) self._reverse_dict = lookups self._namespace_dict = namespaces self._app_dict = apps diff --git a/django/core/validators.py b/django/core/validators.py index b1b82dbf0df0..27a42c4cda05 100644 --- a/django/core/validators.py +++ b/django/core/validators.py @@ -41,7 +41,7 @@ def __call__(self, value): class URLValidator(RegexValidator): regex = re.compile( r'^https?://' # http:// or https:// - r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' #domain... + r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' #domain... r'localhost|' #localhost... r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip r'(?::\d+)?' # optional port @@ -81,7 +81,7 @@ def __call__(self, value): "User-Agent": self.user_agent, } try: - req = urllib2.Request(url, None, headers) + req = urllib2.Request(url.encode('utf-8'), None, headers) u = urllib2.urlopen(req) except ValueError: raise ValidationError(_(u'Enter a valid URL.'), code='invalid') diff --git a/django/db/__init__.py b/django/db/__init__.py index 4bae04ab9a2f..9bfe4659bf05 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -12,11 +12,12 @@ # For backwards compatibility - Port any old database settings over to # the new values. if not settings.DATABASES: - import warnings - warnings.warn( - "settings.DATABASE_* is deprecated; use settings.DATABASES instead.", - PendingDeprecationWarning - ) + if settings.DATABASE_ENGINE: + import warnings + warnings.warn( + "settings.DATABASE_* is deprecated; use settings.DATABASES instead.", + PendingDeprecationWarning + ) settings.DATABASES[DEFAULT_DB_ALIAS] = { 'ENGINE': settings.DATABASE_ENGINE, @@ -32,9 +33,11 @@ } if DEFAULT_DB_ALIAS not in settings.DATABASES: - raise ImproperlyConfigured("You must default a '%s' database" % DEFAULT_DB_ALIAS) + raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS) for alias, database in settings.DATABASES.items(): + if 'ENGINE' not in database: + raise ImproperlyConfigured("You must specify a 'ENGINE' for database '%s'" % alias) if database['ENGINE'] in ("postgresql", "postgresql_psycopg2", "sqlite3", "mysql", "oracle"): import warnings if 'django.contrib.gis' in settings.INSTALLED_APPS: diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index fe2c7c451bde..0d9f53384a24 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -22,7 +22,7 @@ def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS): self.alias = alias def __eq__(self, other): - return self.settings_dict == other.settings_dict + return self.alias == other.alias def __ne__(self, other): return not self == other @@ -233,6 +233,13 @@ def lookup_cast(self, lookup_type): """ return "%s" + def max_in_list_size(self): + """ + Returns the maximum number of items that can be passed in a single 'IN' + list condition, or None if the backend does not impose a limit. + """ + return None + def max_name_length(self): """ Returns the maximum length of table and column names, or None if there diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 492ac1e3e93c..7da44f8c32fd 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -350,12 +350,28 @@ def create_test_db(self, verbosity=1, autoclobber=False): can_rollback = self._rollback_works() self.connection.settings_dict["SUPPORTS_TRANSACTIONS"] = can_rollback - call_command('syncdb', verbosity=verbosity, interactive=False, database=self.connection.alias) + call_command('syncdb', + verbosity=verbosity, + interactive=False, + database=self.connection.alias, + load_initial_data=False) + + # We need to then do a flush to ensure that any data installed by + # custom SQL has been removed. The only test data should come from + # test fixtures, or autogenerated from post_syncdb triggers. + # This has the side effect of loading initial data (which was + # intentionally skipped in the syncdb). + call_command('flush', + verbosity=verbosity, + interactive=False, + database=self.connection.alias) if settings.CACHE_BACKEND.startswith('db://'): - from django.core.cache import parse_backend_uri - _, cache_name, _ = parse_backend_uri(settings.CACHE_BACKEND) - call_command('createcachetable', cache_name) + from django.core.cache import parse_backend_uri, cache + from django.db import router + if router.allow_syncdb(self.connection.alias, cache.cache_model_class): + _, cache_name, _ = parse_backend_uri(settings.CACHE_BACKEND) + call_command('createcachetable', cache_name, database=self.connection.alias) # Get a cursor (even though we don't need one yet). This has # the side effect of initializing the test database. @@ -452,3 +468,17 @@ def set_autocommit(self): def sql_table_creation_suffix(self): "SQL to append to the end of the test table creation statements" return '' + + def test_db_signature(self): + """ + Returns a tuple with elements of self.connection.settings_dict (a + DATABASES setting value) that uniquely identify a database + accordingly to the RDBMS particularities. + """ + settings_dict = self.connection.settings_dict + return ( + settings_dict['HOST'], + settings_dict['PORT'], + settings_dict['ENGINE'], + settings_dict['NAME'] + ) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index e94e24bff940..a39c41f8d8a7 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -297,7 +297,7 @@ def _cursor(self): self.connection = Database.connect(**kwargs) self.connection.encoders[SafeUnicode] = self.connection.encoders[unicode] self.connection.encoders[SafeString] = self.connection.encoders[str] - connection_created.send(sender=self.__class__) + connection_created.send(sender=self.__class__, connection=self) cursor = CursorWrapper(self.connection.cursor()) return cursor diff --git a/django/db/backends/mysql/client.py b/django/db/backends/mysql/client.py index ff5b64d1e081..1cf8ceef9c80 100644 --- a/django/db/backends/mysql/client.py +++ b/django/db/backends/mysql/client.py @@ -24,7 +24,10 @@ def runshell(self): if passwd: args += ["--password=%s" % passwd] if host: - args += ["--host=%s" % host] + if '/' in host: + args += ["--socket=%s" % host] + else: + args += ["--host=%s" % host] if port: args += ["--port=%s" % port] if db: diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 369e65baf744..ea96f6eaeffa 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -6,16 +6,38 @@ import datetime -import os import sys import time from decimal import Decimal -# Oracle takes client-side character set encoding from the environment. -os.environ['NLS_LANG'] = '.UTF8' -# This prevents unicode from getting mangled by getting encoded into the -# potentially non-unicode database character set. -os.environ['ORA_NCHAR_LITERAL_REPLACE'] = 'TRUE' + +def _setup_environment(environ): + import platform + # Cygwin requires some special voodoo to set the environment variables + # properly so that Oracle will see them. + if platform.system().upper().startswith('CYGWIN'): + try: + import ctypes + except ImportError, e: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured("Error loading ctypes: %s; " + "the Oracle backend requires ctypes to " + "operate correctly under Cygwin." % e) + kernel32 = ctypes.CDLL('kernel32') + for name, value in environ: + kernel32.SetEnvironmentVariableA(name, value) + else: + import os + os.environ.update(environ) + +_setup_environment([ + # Oracle takes client-side character set encoding from the environment. + ('NLS_LANG', '.UTF8'), + # This prevents unicode from getting mangled by getting encoded into the + # potentially non-unicode database character set. + ('ORA_NCHAR_LITERAL_REPLACE', 'TRUE'), +]) + try: import cx_Oracle as Database @@ -178,6 +200,9 @@ def lookup_cast(self, lookup_type): return "UPPER(%s)" return "%s" + def max_in_list_size(self): + return 1000 + def max_name_length(self): return 30 @@ -310,9 +335,24 @@ def combine_expression(self, connector, sub_expressions): return super(DatabaseOperations, self).combine_expression(connector, sub_expressions) +class _UninitializedOperatorsDescriptor(object): + + def __get__(self, instance, owner): + # If connection.operators is looked up before a connection has been + # created, transparently initialize connection.operators to avert an + # AttributeError. + if instance is None: + raise AttributeError("operators not available as class attribute") + # Creating a cursor will initialize the operators. + instance.cursor().close() + return instance.__dict__['operators'] + + class DatabaseWrapper(BaseDatabaseWrapper): - operators = { + operators = _UninitializedOperatorsDescriptor() + + _standard_operators = { 'exact': '= %s', 'iexact': '= UPPER(%s)', 'contains': "LIKE TRANSLATE(%s USING NCHAR_CS) ESCAPE TRANSLATE('\\' USING NCHAR_CS)", @@ -326,11 +366,21 @@ class DatabaseWrapper(BaseDatabaseWrapper): 'istartswith': "LIKE UPPER(TRANSLATE(%s USING NCHAR_CS)) ESCAPE TRANSLATE('\\' USING NCHAR_CS)", 'iendswith': "LIKE UPPER(TRANSLATE(%s USING NCHAR_CS)) ESCAPE TRANSLATE('\\' USING NCHAR_CS)", } - oracle_version = None + + _likec_operators = _standard_operators.copy() + _likec_operators.update({ + 'contains': "LIKEC %s ESCAPE '\\'", + 'icontains': "LIKEC UPPER(%s) ESCAPE '\\'", + 'startswith': "LIKEC %s ESCAPE '\\'", + 'endswith': "LIKEC %s ESCAPE '\\'", + 'istartswith': "LIKEC UPPER(%s) ESCAPE '\\'", + 'iendswith': "LIKEC UPPER(%s) ESCAPE '\\'", + }) def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) + self.oracle_version = None self.features = DatabaseFeatures() self.ops = DatabaseOperations() self.client = DatabaseClient(self) @@ -343,9 +393,9 @@ def _valid_connection(self): def _connect_string(self): settings_dict = self.settings_dict - if len(settings_dict['HOST'].strip()) == 0: + if not settings_dict['HOST'].strip(): settings_dict['HOST'] = 'localhost' - if len(settings_dict['PORT'].strip()) != 0: + if settings_dict['PORT'].strip(): dsn = Database.makedsn(settings_dict['HOST'], int(settings_dict['PORT']), settings_dict['NAME']) @@ -366,6 +416,22 @@ def _cursor(self): cursor.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS' " "NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF' " "NLS_TERRITORY = 'AMERICA'") + + if 'operators' not in self.__dict__: + # Ticket #14149: Check whether our LIKE implementation will + # work for this connection or we need to fall back on LIKEC. + # This check is performed only once per DatabaseWrapper + # instance per thread, since subsequent connections will use + # the same settings. + try: + cursor.execute("SELECT 1 FROM DUAL WHERE DUMMY %s" + % self._standard_operators['contains'], + ['X']) + except utils.DatabaseError: + self.operators = self._likec_operators + else: + self.operators = self._standard_operators + try: self.oracle_version = int(self.connection.version.split('.')[0]) # There's no way for the DatabaseOperations class to know the @@ -384,7 +450,7 @@ def _cursor(self): # Django docs specify cx_Oracle version 4.3.1 or higher, but # stmtcachesize is available only in 4.3.2 and up. pass - connection_created.send(sender=self.__class__) + connection_created.send(sender=self.__class__, connection=self) if not cursor: cursor = FormatStylePlaceholderCursor(self.connection) return cursor @@ -393,6 +459,28 @@ def _cursor(self): def _savepoint_commit(self, sid): pass + def _commit(self): + if self.connection is not None: + try: + return self.connection.commit() + except Database.IntegrityError, e: + # In case cx_Oracle implements (now or in a future version) + # raising this specific exception + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + except Database.DatabaseError, e: + # cx_Oracle 5.0.4 raises a cx_Oracle.DatabaseError exception + # with the following attributes and values: + # code = 2091 + # message = 'ORA-02091: transaction rolled back + # 'ORA-02291: integrity constraint (TEST_DJANGOTEST.SYS + # _C00102056) violated - parent key not found' + # We convert that particular case to our IntegrityError exception + x = e.args[0] + if hasattr(x, 'code') and hasattr(x, 'message') \ + and x.code == 2091 and 'ORA-02291' in x.message: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2] + class OracleParam(object): """ @@ -641,19 +729,15 @@ def _get_sequence_reset_sql(): # TODO: colorize this SQL code with style.SQL_KEYWORD(), etc. return """ DECLARE - startvalue integer; - cval integer; + table_value integer; + seq_value integer; BEGIN - LOCK TABLE %(table)s IN SHARE MODE; - SELECT NVL(MAX(%(column)s), 0) INTO startvalue FROM %(table)s; - SELECT "%(sequence)s".nextval INTO cval FROM dual; - cval := startvalue - cval; - IF cval != 0 THEN - EXECUTE IMMEDIATE 'ALTER SEQUENCE "%(sequence)s" MINVALUE 0 INCREMENT BY '||cval; - SELECT "%(sequence)s".nextval INTO cval FROM dual; - EXECUTE IMMEDIATE 'ALTER SEQUENCE "%(sequence)s" INCREMENT BY 1'; - END IF; - COMMIT; + SELECT NVL(MAX(%(column)s), 0) INTO table_value FROM %(table)s; + SELECT NVL(last_number - cache_size, 0) INTO seq_value FROM user_sequences + WHERE sequence_name = '%(sequence)s'; + WHILE table_value > seq_value LOOP + SELECT "%(sequence)s".nextval INTO seq_value FROM dual; + END LOOP; END; /""" diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index e6e242b9f72e..db64912a13e7 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -1,5 +1,4 @@ import sys, time -from django.core import management from django.db.backends.creation import BaseDatabaseCreation TEST_DATABASE_PREFIX = 'test_' @@ -39,7 +38,9 @@ class DatabaseCreation(BaseDatabaseCreation): 'URLField': 'VARCHAR2(%(max_length)s)', } - remember = {} + def __init__(self, connection): + self.remember = {} + super(DatabaseCreation, self).__init__(connection) def _create_test_db(self, verbosity=1, autoclobber=False): TEST_NAME = self._test_database_name() @@ -113,6 +114,16 @@ def _create_test_db(self, verbosity=1, autoclobber=False): return self.connection.settings_dict['NAME'] + def test_db_signature(self): + settings_dict = self.connection.settings_dict + return ( + settings_dict['HOST'], + settings_dict['PORT'], + settings_dict['ENGINE'], + settings_dict['NAME'], + self._test_database_user(), + ) + def _destroy_test_db(self, test_database_name, verbosity=1): """ Destroy a test database, prompting the user for confirmation if the @@ -135,9 +146,6 @@ def _destroy_test_db(self, test_database_name, verbosity=1): 'tblspace_temp': TEST_TBLSPACE_TMP, } - self.remember['user'] = self.connection.settings_dict['USER'] - self.remember['passwd'] = self.connection.settings_dict['PASSWORD'] - cursor = self.connection.cursor() time.sleep(1) # To avoid "database is being accessed by other users" errors. if self._test_user_create(): @@ -156,7 +164,7 @@ def _execute_test_db_creation(self, cursor, parameters, verbosity): statements = [ """CREATE TABLESPACE %(tblspace)s DATAFILE '%(tblspace)s.dbf' SIZE 20M - REUSE AUTOEXTEND ON NEXT 10M MAXSIZE 100M + REUSE AUTOEXTEND ON NEXT 10M MAXSIZE 200M """, """CREATE TEMPORARY TABLESPACE %(tblspace_temp)s TEMPFILE '%(tblspace_temp)s.dbf' SIZE 20M @@ -214,35 +222,13 @@ def _test_database_name(self): name = self.connection.settings_dict['TEST_NAME'] except AttributeError: pass - except: - raise return name def _test_database_create(self): - name = True - try: - if self.connection.settings_dict['TEST_CREATE']: - name = True - else: - name = False - except KeyError: - pass - except: - raise - return name + return self.connection.settings_dict.get('TEST_CREATE', True) def _test_user_create(self): - name = True - try: - if self.connection.settings_dict['TEST_USER_CREATE']: - name = True - else: - name = False - except KeyError: - pass - except: - raise - return name + return self.connection.settings_dict.get('TEST_USER_CREATE', True) def _test_database_user(self): name = TEST_DATABASE_PREFIX + self.connection.settings_dict['USER'] @@ -251,8 +237,6 @@ def _test_database_user(self): name = self.connection.settings_dict['TEST_USER'] except KeyError: pass - except: - raise return name def _test_database_passwd(self): @@ -262,8 +246,6 @@ def _test_database_passwd(self): name = self.connection.settings_dict['TEST_PASSWD'] except KeyError: pass - except: - raise return name def _test_database_tblspace(self): @@ -273,8 +255,6 @@ def _test_database_tblspace(self): name = self.connection.settings_dict['TEST_TBLSPACE'] except KeyError: pass - except: - raise return name def _test_database_tblspace_tmp(self): @@ -284,6 +264,4 @@ def _test_database_tblspace_tmp(self): name = self.connection.settings_dict['TEST_TBLSPACE_TMP'] except KeyError: pass - except: - raise return name diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index 0dd0304302bd..b8a8b2e2c1f1 100644 --- a/django/db/backends/oracle/introspection.py +++ b/django/db/backends/oracle/introspection.py @@ -66,6 +66,7 @@ def get_relations(self, cursor, table_name): Returns a dictionary of {field_index: (field_index_other_table, other_table)} representing all relationships to the given table. Indexes are 0-based. """ + table_name = table_name.upper() cursor.execute(""" SELECT ta.column_id - 1, tb.table_name, tb.column_id - 1 FROM user_constraints, USER_CONS_COLUMNS ca, USER_CONS_COLUMNS cb, @@ -82,7 +83,7 @@ def get_relations(self, cursor, table_name): relations = {} for row in cursor.fetchall(): - relations[row[0]] = (row[2], row[1]) + relations[row[0]] = (row[2], row[1].lower()) return relations def get_indexes(self, cursor, table_name): diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py index a1c858bd8ff4..5f4d791f02f8 100644 --- a/django/db/backends/postgresql/base.py +++ b/django/db/backends/postgresql/base.py @@ -135,8 +135,9 @@ def _cursor(self): if settings_dict['PORT']: conn_string += " port=%s" % settings_dict['PORT'] self.connection = Database.connect(conn_string, **settings_dict['OPTIONS']) - self.connection.set_isolation_level(1) # make transactions transparent to all cursors - connection_created.send(sender=self.__class__) + # make transactions transparent to all cursors + self.connection.set_isolation_level(1) + connection_created.send(sender=self.__class__, connection=self) cursor = self.connection.cursor() if new_connection: if set_tz: @@ -149,6 +150,13 @@ def _cursor(self): cursor.execute("SET client_encoding to 'UNICODE'") return UnicodeCursorWrapper(cursor, 'utf-8') + def _commit(self): + if self.connection is not None: + try: + return self.connection.commit() + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] + def typecast_string(s): """ Cast all returned strings to unicode strings. diff --git a/django/db/backends/postgresql/creation.py b/django/db/backends/postgresql/creation.py index af26d0b78f5f..1a821afe07d9 100644 --- a/django/db/backends/postgresql/creation.py +++ b/django/db/backends/postgresql/creation.py @@ -63,7 +63,7 @@ def get_index_sql(index_name, opclass=''): # a second index that specifies their operator class, which is # needed when performing correct LIKE queries outside the # C locale. See #12234. - db_type = f.db_type() + db_type = f.db_type(connection=self.connection) if db_type.startswith('varchar'): output.append(get_index_sql('%s_%s_like' % (db_table, f.column), ' varchar_pattern_ops')) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 29b7e7ff1a52..c9f1af1669f0 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -136,7 +136,7 @@ def _cursor(self): self.connection = Database.connect(**conn_params) self.connection.set_client_encoding('UTF8') self.connection.set_isolation_level(self.isolation_level) - connection_created.send(sender=self.__class__) + connection_created.send(sender=self.__class__, connection=self) cursor = self.connection.cursor() cursor.tzinfo_factory = None if new_connection: @@ -189,3 +189,10 @@ def _set_isolation_level(self, level): finally: self.isolation_level = level self.features.uses_savepoints = bool(level) + + def _commit(self): + if self.connection is not None: + try: + return self.connection.commit() + except Database.IntegrityError, e: + raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2] diff --git a/django/db/backends/signals.py b/django/db/backends/signals.py index a8079d0d76fd..c16a63f9f67f 100644 --- a/django/db/backends/signals.py +++ b/django/db/backends/signals.py @@ -1,3 +1,3 @@ from django.dispatch import Signal -connection_created = Signal() +connection_created = Signal(providing_args=["connection"]) diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index bc97f5cfd80c..1ab255762769 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -176,7 +176,7 @@ def _cursor(self): self.connection.create_function("django_extract", 2, _sqlite_extract) self.connection.create_function("django_date_trunc", 2, _sqlite_date_trunc) self.connection.create_function("regexp", 2, _sqlite_regexp) - connection_created.send(sender=self.__class__) + connection_created.send(sender=self.__class__, connection=self) return self.connection.cursor(factory=SQLiteCursorWrapper) def close(self): diff --git a/django/db/backends/util.py b/django/db/backends/util.py index 7773273ba677..aad668e771ae 100644 --- a/django/db/backends/util.py +++ b/django/db/backends/util.py @@ -81,7 +81,7 @@ def typecast_timestamp(s): # does NOT store time zone information else: microseconds = '0' return datetime.datetime(int(dates[0]), int(dates[1]), int(dates[2]), - int(times[0]), int(times[1]), int(seconds), int(float('.'+microseconds) * 1000000)) + int(times[0]), int(times[1]), int(seconds), int((microseconds + '000000')[:6])) def typecast_boolean(s): if s is None: return None diff --git a/django/db/models/base.py b/django/db/models/base.py index 6304e009d3d8..e857a875a108 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1,12 +1,13 @@ import types import sys -import os from itertools import izip + import django.db.models.manager # Imported to register signal handler. from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, FieldError, ValidationError, NON_FIELD_ERRORS from django.core import validators from django.db.models.fields import AutoField, FieldDoesNotExist -from django.db.models.fields.related import OneToOneRel, ManyToOneRel, OneToOneField +from django.db.models.fields.related import (OneToOneRel, ManyToOneRel, + OneToOneField, add_lazy_relation) from django.db.models.query import delete_objects, Q from django.db.models.query_utils import CollectedObjects, DeferredAttribute from django.db.models.options import Options @@ -16,7 +17,7 @@ from django.utils.translation import ugettext_lazy as _ import django.utils.copycompat as copy from django.utils.functional import curry, update_wrapper -from django.utils.encoding import smart_str, force_unicode, smart_unicode +from django.utils.encoding import smart_str, force_unicode from django.utils.text import get_text_list, capfirst from django.conf import settings @@ -224,8 +225,25 @@ def _prepare(cls): if opts.order_with_respect_to: cls.get_next_in_order = curry(cls._get_next_or_previous_in_order, is_next=True) cls.get_previous_in_order = curry(cls._get_next_or_previous_in_order, is_next=False) - setattr(opts.order_with_respect_to.rel.to, 'get_%s_order' % cls.__name__.lower(), curry(method_get_order, cls)) - setattr(opts.order_with_respect_to.rel.to, 'set_%s_order' % cls.__name__.lower(), curry(method_set_order, cls)) + # defer creating accessors on the foreign class until we are + # certain it has been created + def make_foreign_order_accessors(field, model, cls): + setattr( + field.rel.to, + 'get_%s_order' % cls.__name__.lower(), + curry(method_get_order, cls) + ) + setattr( + field.rel.to, + 'set_%s_order' % cls.__name__.lower(), + curry(method_set_order, cls) + ) + add_lazy_relation( + cls, + opts.order_with_respect_to, + opts.order_with_respect_to.rel.to, + make_foreign_order_accessors + ) # Give the class a docstring -- its definition. if cls.__doc__ is None: @@ -243,6 +261,10 @@ class ModelState(object): """ def __init__(self, db=None): self.db = db + # If true, uniqueness validation checks will consider this a new, as-yet-unsaved object. + # Necessary for correct validation of new instances of objects with explicit (non-auto) PKs. + # This impacts validation only; it has no effect on the actual save. + self.adding = True class Model(object): __metaclass__ = ModelBase @@ -338,6 +360,7 @@ def __init__(self, *args, **kwargs): pass if kwargs: raise TypeError("'%s' is an invalid keyword argument for this function" % kwargs.keys()[0]) + super(Model, self).__init__() signals.post_init.send(sender=self.__class__, instance=self) def __repr__(self): @@ -536,12 +559,15 @@ def save_base(self, raw=False, cls=None, origin=None, force_insert=False, # Store the database on which the object was saved self._state.db = using + # Once saved, this is no longer a to-be-added instance. + self._state.adding = False # Signal that the save is complete if origin and not meta.auto_created: signals.post_save.send(sender=origin, instance=self, created=(not record_exists), raw=raw) + save_base.alters_data = True def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): @@ -588,7 +614,7 @@ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): for related in self._meta.get_all_related_many_to_many_objects(): if related.field.rel.through: - db = router.db_for_write(related.field.rel.through.__class__, instance=self) + db = router.db_for_write(related.field.rel.through, instance=self) opts = related.field.rel.through._meta reverse_field_name = related.field.m2m_reverse_field_name() nullable = opts.get_field(reverse_field_name).null @@ -598,7 +624,7 @@ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False): for f in self._meta.many_to_many: if f.rel.through: - db = router.db_for_write(f.rel.through.__class__, instance=self) + db = router.db_for_write(f.rel.through, instance=self) opts = f.rel.through._meta field_name = f.m2m_field_name() nullable = opts.get_field(field_name).null @@ -706,7 +732,7 @@ def _get_unique_checks(self, exclude=None): called from a ModelForm, some fields may have been excluded; we can't perform a unique check on a model that is missing fields involved in that check. - Fields that did not validate should also be exluded, but they need + Fields that did not validate should also be excluded, but they need to be passed in via the exclude argument. """ if exclude is None: @@ -744,11 +770,11 @@ def _get_unique_checks(self, exclude=None): continue if f.unique: unique_checks.append((model_class, (name,))) - if f.unique_for_date: + if f.unique_for_date and f.unique_for_date not in exclude: date_checks.append((model_class, 'date', name, f.unique_for_date)) - if f.unique_for_year: + if f.unique_for_year and f.unique_for_year not in exclude: date_checks.append((model_class, 'year', name, f.unique_for_year)) - if f.unique_for_month: + if f.unique_for_month and f.unique_for_month not in exclude: date_checks.append((model_class, 'month', name, f.unique_for_month)) return unique_checks, date_checks @@ -766,7 +792,7 @@ def _perform_unique_checks(self, unique_checks): if lookup_value is None: # no value, skip the lookup continue - if f.primary_key and not getattr(self, '_adding', False): + if f.primary_key and not self._state.adding: # no need to check for unique primary key when editing continue lookup_kwargs[str(field_name)] = lookup_value @@ -779,7 +805,7 @@ def _perform_unique_checks(self, unique_checks): # Exclude the current object from the query if we are editing an # instance (as opposed to creating a new one) - if not getattr(self, '_adding', False) and self.pk is not None: + if not self._state.adding and self.pk is not None: qs = qs.exclude(pk=self.pk) if qs.exists(): @@ -798,6 +824,8 @@ def _perform_date_checks(self, date_checks): # there's a ticket to add a date lookup, we can remove this special # case if that makes it's way in date = getattr(self, unique_for) + if date is None: + continue if lookup_type == 'date': lookup_kwargs['%s__day' % unique_for] = date.day lookup_kwargs['%s__month' % unique_for] = date.month @@ -809,7 +837,7 @@ def _perform_date_checks(self, date_checks): qs = model_class._default_manager.filter(**lookup_kwargs) # Exclude the current object from the query if we are editing an # instance (as opposed to creating a new one) - if not getattr(self, '_adding', False) and self.pk is not None: + if not self._state.adding and self.pk is not None: qs = qs.exclude(pk=self.pk) if qs.exists(): diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index 65b60a01739e..ddb228ac114e 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -459,6 +459,9 @@ def __init__(self, *args, **kwargs): kwargs['blank'] = True Field.__init__(self, *args, **kwargs) + def get_internal_type(self): + return "AutoField" + def to_python(self, value): if value is None: return value @@ -511,7 +514,7 @@ def to_python(self, value): raise exceptions.ValidationError(self.error_messages['invalid']) def get_prep_lookup(self, lookup_type, value): - # Special-case handling for filters coming from a web request (e.g. the + # Special-case handling for filters coming from a Web request (e.g. the # admin interface). Only works for scalar values (not lists). If you're # passing in a list, you might as well make things the right type when # constructing the list. @@ -619,7 +622,7 @@ def to_python(self, value): def pre_save(self, model_instance, add): if self.auto_now or (self.auto_now_add and add): - value = datetime.datetime.now() + value = datetime.date.today() setattr(model_instance, self.attname, value) return value else: @@ -706,6 +709,14 @@ def to_python(self, value): except ValueError: raise exceptions.ValidationError(self.error_messages['invalid']) + def pre_save(self, model_instance, add): + if self.auto_now or (self.auto_now_add and add): + value = datetime.datetime.now() + setattr(model_instance, self.attname, value) + return value + else: + return super(DateTimeField, self).pre_save(model_instance, add) + def get_prep_value(self, value): return self.to_python(value) @@ -795,6 +806,14 @@ def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 75) CharField.__init__(self, *args, **kwargs) + def formfield(self, **kwargs): + # As with CharField, this will cause email validation to be performed twice + defaults = { + 'form_class': forms.EmailField, + } + defaults.update(kwargs) + return super(EmailField, self).formfield(**defaults) + class FilePathField(Field): description = _("File path") @@ -935,7 +954,7 @@ def to_python(self, value): raise exceptions.ValidationError(self.error_messages['invalid']) def get_prep_lookup(self, lookup_type, value): - # Special-case handling for filters coming from a web request (e.g. the + # Special-case handling for filters coming from a Web request (e.g. the # admin interface). Only works for scalar values (not lists). If you're # passing in a list, you might as well make things the right type when # constructing the list. @@ -1100,11 +1119,19 @@ def formfield(self, **kwargs): class URLField(CharField): description = _("URL") - def __init__(self, verbose_name=None, name=None, verify_exists=True, **kwargs): + def __init__(self, verbose_name=None, name=None, verify_exists=False, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 200) CharField.__init__(self, verbose_name, name, **kwargs) self.validators.append(validators.URLValidator(verify_exists=verify_exists)) + def formfield(self, **kwargs): + # As with CharField, this will cause URL validation to be performed twice + defaults = { + 'form_class': forms.URLField, + } + defaults.update(kwargs) + return super(URLField, self).formfield(**defaults) + class XMLField(TextField): description = _("XML text") diff --git a/django/db/models/fields/files.py b/django/db/models/fields/files.py index 6dfeddbc419c..28f25252d230 100644 --- a/django/db/models/fields/files.py +++ b/django/db/models/fields/files.py @@ -73,7 +73,7 @@ def _get_url(self): def _get_size(self): self._require_file() if not self._committed: - return len(self.file) + return self.file.size return self.storage.size(self.name) size = property(_get_size) @@ -93,7 +93,7 @@ def save(self, name, content, save=True): setattr(self.instance, self.field.name, self.name) # Update the filesize cache - self._size = len(content) + self._size = content.size self._committed = True # Save the object because it has changed, unless save is False @@ -258,19 +258,6 @@ def pre_save(self, model_instance, add): def contribute_to_class(self, cls, name): super(FileField, self).contribute_to_class(cls, name) setattr(cls, self.name, self.descriptor_class(self)) - signals.post_delete.connect(self.delete_file, sender=cls) - - def delete_file(self, instance, sender, **kwargs): - file = getattr(instance, self.attname) - # If no other object of this type references the file, - # and it's not the default value for future objects, - # delete it from the backend. - if file and file.name != self.default and \ - not sender._default_manager.filter(**{self.name: file.name}): - file.delete(save=False) - elif file: - # Otherwise, just close the file, so it doesn't tie up resources. - file.close() def get_directory_name(self): return os.path.normpath(force_unicode(datetime.datetime.now().strftime(smart_str(self.upload_to)))) diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 5830a794dfed..ed3a9a1af208 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -176,9 +176,20 @@ def _pk_trace(self, value, prep_func, lookup_type, **kwargs): # the primary key may itself be an object - so we need to keep drilling # down until we hit a value that can be used for a comparison. v = value + + # In the case of an FK to 'self', this check allows to_field to be used + # for both forwards and reverse lookups across the FK. (For normal FKs, + # it's only relevant for forward lookups). + if isinstance(v, self.rel.to): + field_name = getattr(self.rel, "field_name", None) + else: + field_name = None try: while True: - v = getattr(v, v._meta.pk.name) + if field_name is None: + field_name = v._meta.pk.name + v = getattr(v, field_name) + field_name = None except AttributeError: pass except exceptions.ObjectDoesNotExist: @@ -420,7 +431,7 @@ def add(self, *objs): def create(self, **kwargs): kwargs.update({rel_field.name: instance}) db = router.db_for_write(rel_model, instance=instance) - return super(RelatedManager, self).using(db).create(**kwargs) + return super(RelatedManager, self.db_manager(db)).create(**kwargs) create.alters_data = True def get_or_create(self, **kwargs): @@ -428,7 +439,7 @@ def get_or_create(self, **kwargs): # ForeignRelatedObjectsDescriptor knows about. kwargs.update({rel_field.name: instance}) db = router.db_for_write(rel_model, instance=instance) - return super(RelatedManager, self).using(db).get_or_create(**kwargs) + return super(RelatedManager, self.db_manager(db)).get_or_create(**kwargs) get_or_create.alters_data = True # remove() and clear() are only provided if the ForeignKey can have a value of null. @@ -517,7 +528,7 @@ def create(self, **kwargs): opts = through._meta raise AttributeError("Cannot use create() on a ManyToManyField which specifies an intermediary model. Use %s.%s's Manager instead." % (opts.app_label, opts.object_name)) db = router.db_for_write(self.instance.__class__, instance=self.instance) - new_obj = super(ManyRelatedManager, self).using(db).create(**kwargs) + new_obj = super(ManyRelatedManager, self.db_manager(db)).create(**kwargs) self.add(new_obj) return new_obj create.alters_data = True @@ -525,7 +536,7 @@ def create(self, **kwargs): def get_or_create(self, **kwargs): db = router.db_for_write(self.instance.__class__, instance=self.instance) obj, created = \ - super(ManyRelatedManager, self).using(db).get_or_create(**kwargs) + super(ManyRelatedManager, self.db_manager(db)).get_or_create(**kwargs) # We only need to add() if created because if we got an object back # from get() then the relationship already exists. if created: @@ -553,7 +564,7 @@ def _add_items(self, source_field_name, target_field_name, *objs): raise TypeError("'%s' instance expected" % self.model._meta.object_name) else: new_ids.add(obj) - db = router.db_for_write(self.through.__class__, instance=self.instance) + db = router.db_for_write(self.through, instance=self.instance) vals = self.through._default_manager.using(db).values_list(target_field_name, flat=True) vals = vals.filter(**{ source_field_name: self._pk_val, @@ -601,7 +612,7 @@ def _remove_items(self, source_field_name, target_field_name, *objs): instance=self.instance, reverse=self.reverse, model=self.model, pk_set=old_ids) # Remove the specified objects from the join table - db = router.db_for_write(self.through.__class__, instance=self.instance) + db = router.db_for_write(self.through, instance=self.instance) self.through._default_manager.using(db).filter(**{ source_field_name: self._pk_val, '%s__in' % target_field_name: old_ids @@ -621,7 +632,7 @@ def _clear_items(self, source_field_name): signals.m2m_changed.send(sender=rel.through, action="pre_clear", instance=self.instance, reverse=self.reverse, model=self.model, pk_set=None) - db = router.db_for_write(self.through.__class__, instance=self.instance) + db = router.db_for_write(self.through, instance=self.instance) self.through._default_manager.using(db).filter(**{ source_field_name: self._pk_val }).delete() @@ -812,6 +823,9 @@ def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): to_field = to_field or (to._meta.pk and to._meta.pk.name) kwargs['verbose_name'] = kwargs.get('verbose_name', None) + if 'db_index' not in kwargs: + kwargs['db_index'] = True + kwargs['rel'] = rel_class(to, to_field, related_name=kwargs.pop('related_name', None), limit_choices_to=kwargs.pop('limit_choices_to', None), @@ -819,8 +833,6 @@ def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): parent_link=kwargs.pop('parent_link', False)) Field.__init__(self, **kwargs) - self.db_index = True - def validate(self, value, model_instance): if self.rel.parent_link: return @@ -828,7 +840,10 @@ def validate(self, value, model_instance): if value is None: return - qs = self.rel.to._default_manager.filter(**{self.rel.field_name:value}) + using = router.db_for_read(model_instance.__class__, instance=model_instance) + qs = self.rel.to._default_manager.using(using).filter( + **{self.rel.field_name: value} + ) qs = qs.complex_filter(self.rel.limit_choices_to) if not qs.exists(): raise exceptions.ValidationError(self.error_messages['invalid'] % { @@ -880,6 +895,8 @@ def contribute_to_related_class(self, cls, related): # don't get a related descriptor. if not self.rel.is_hidden(): setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) + if self.rel.limit_choices_to: + cls._meta.related_fkey_lookups.append(self.rel.limit_choices_to) if self.rel.field_name is None: self.rel.field_name = cls._meta.pk.name @@ -1129,6 +1146,11 @@ def contribute_to_related_class(self, cls, related): self.m2m_field_name = curry(self._get_m2m_attr, related, 'name') self.m2m_reverse_field_name = curry(self._get_m2m_reverse_attr, related, 'name') + get_m2m_rel = curry(self._get_m2m_attr, related, 'rel') + self.m2m_target_field_name = lambda: get_m2m_rel().field_name + get_m2m_reverse_rel = curry(self._get_m2m_reverse_attr, related, 'rel') + self.m2m_reverse_target_field_name = lambda: get_m2m_reverse_rel().field_name + def set_attributes_from_rel(self): pass diff --git a/django/db/models/fields/subclassing.py b/django/db/models/fields/subclassing.py index bd11675ad3a0..8647cb3c0205 100644 --- a/django/db/models/fields/subclassing.py +++ b/django/db/models/fields/subclassing.py @@ -63,8 +63,8 @@ class LegacyConnection(type): A metaclass to normalize arguments give to the get_db_prep_* and db_type methods on fields. """ - def __new__(cls, names, bases, attrs): - new_cls = super(LegacyConnection, cls).__new__(cls, names, bases, attrs) + def __new__(cls, name, bases, attrs): + new_cls = super(LegacyConnection, cls).__new__(cls, name, bases, attrs) for attr in ('db_type', 'get_db_prep_save'): setattr(new_cls, attr, call_with_connection(getattr(new_cls, attr))) for attr in ('get_db_prep_lookup', 'get_db_prep_value'): @@ -76,10 +76,11 @@ class SubfieldBase(LegacyConnection): A metaclass for custom Field subclasses. This ensures the model's attribute has the descriptor protocol attached to it. """ - def __new__(cls, base, name, attrs): - new_class = super(SubfieldBase, cls).__new__(cls, base, name, attrs) + def __new__(cls, name, bases, attrs): + new_class = super(SubfieldBase, cls).__new__(cls, name, bases, attrs) new_class.contribute_to_class = make_contrib( - attrs.get('contribute_to_class')) + new_class, attrs.get('contribute_to_class') + ) return new_class class Creator(object): @@ -97,7 +98,7 @@ def __get__(self, obj, type=None): def __set__(self, obj, value): obj.__dict__[self.field.name] = self.field.to_python(value) -def make_contrib(func=None): +def make_contrib(superclass, func=None): """ Returns a suitable contribute_to_class() method for the Field subclass. @@ -110,7 +111,7 @@ def contribute_to_class(self, cls, name): if func: func(self, cls, name) else: - super(self.__class__, self).contribute_to_class(cls, name) + super(superclass, self).contribute_to_class(cls, name) setattr(cls, self.name, Creator(self)) return contribute_to_class diff --git a/django/db/models/options.py b/django/db/models/options.py index 5d84ab647202..5ff4b73161c6 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -14,7 +14,7 @@ # Calculate the verbose_name by converting from InitialCaps to "lowercase with spaces". get_verbose_name = lambda class_name: re.sub('(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))', ' \\1', class_name).lower().strip() -DEFAULT_NAMES = ('verbose_name', 'db_table', 'ordering', +DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering', 'unique_together', 'permissions', 'get_latest_by', 'order_with_respect_to', 'app_label', 'db_tablespace', 'abstract', 'managed', 'proxy', 'auto_created') @@ -50,6 +50,10 @@ def __init__(self, meta, app_label=None): self.abstract_managers = [] self.concrete_managers = [] + # List of all lookups defined in ForeignKey 'limit_choices_to' options + # from *other* models. Needed for some admin checks. Internal use only. + self.related_fkey_lookups = [] + def contribute_to_class(self, cls, name): from django.db import connection from django.db.backends.util import truncate_name @@ -86,7 +90,8 @@ def contribute_to_class(self, cls, name): # verbose_name_plural is a special case because it uses a 's' # by default. - setattr(self, 'verbose_name_plural', meta_attrs.pop('verbose_name_plural', string_concat(self.verbose_name, 's'))) + if self.verbose_name_plural is None: + self.verbose_name_plural = string_concat(self.verbose_name, 's') # Any leftover attributes must be invalid. if meta_attrs != {}: @@ -113,6 +118,12 @@ def _prepare(self, model): # Promote the first parent link in lieu of adding yet another # field. field = self.parents.value_for_index(0) + # Look for a local field with the same name as the + # first parent link. If a local field has already been + # created, use it instead of promoting the parent + already_created = [fld for fld in self.local_fields if fld.name == field.name] + if already_created: + field = already_created[0] field.primary_key = True self.setup_pk(field) else: diff --git a/django/db/models/query.py b/django/db/models/query.py index d9fbd9b8cb43..a2d7ffbf42cd 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -2,7 +2,6 @@ The main QuerySet implementation. This provides the public API for the ORM. """ -from copy import deepcopy from itertools import izip from django.db import connections, router, transaction, IntegrityError @@ -80,7 +79,7 @@ def __len__(self): else: self._result_cache = list(self.iterator()) elif self._iter: - self._result_cache.extend(list(self._iter)) + self._result_cache.extend(self._iter) return len(self._result_cache) def __iter__(self): @@ -265,11 +264,14 @@ def iterator(self): init_list.append(field.attname) model_cls = deferred_class_factory(self.model, skip) - compiler = self.query.get_compiler(using=self.db) + # Cache db and model outside the loop + db = self.db + model = self.model + compiler = self.query.get_compiler(using=db) for row in compiler.results_iter(): if fill_cache: - obj, _ = get_cached_row(self.model, row, - index_start, using=self.db, max_depth=max_depth, + obj, _ = get_cached_row(model, row, + index_start, using=db, max_depth=max_depth, requested=requested, offset=len(aggregate_select), only_load=only_load) else: @@ -279,17 +281,21 @@ def iterator(self): obj = model_cls(**dict(zip(init_list, row_data))) else: # Omit aggregates in object creation. - obj = self.model(*row[index_start:aggregate_start]) + obj = model(*row[index_start:aggregate_start]) # Store the source database of the object - obj._state.db = self.db + obj._state.db = db + # This object came from the database; it's not being added. + obj._state.adding = False - for i, k in enumerate(extra_select): - setattr(obj, k, row[i]) + if extra_select: + for i, k in enumerate(extra_select): + setattr(obj, k, row[i]) # Add the aggregates to the model - for i, aggregate in enumerate(aggregate_select): - setattr(obj, aggregate, row[i+aggregate_start]) + if aggregate_select: + for i, aggregate in enumerate(aggregate_select): + setattr(obj, aggregate, row[i+aggregate_start]) yield obj @@ -361,9 +367,13 @@ def get_or_create(self, **kwargs): 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: + if f.attname in lookup: + lookup[f.name] = lookup.pop(f.attname) try: self._for_write = True - return self.get(**kwargs), False + return self.get(**lookup), False except self.model.DoesNotExist: try: params = dict([(k, v) for k, v in kwargs.items() if '__' not in k]) @@ -376,7 +386,7 @@ def get_or_create(self, **kwargs): except IntegrityError, e: transaction.savepoint_rollback(sid, using=self.db) try: - return self.get(**kwargs), False + return self.get(**lookup), False except self.model.DoesNotExist: raise e @@ -407,6 +417,7 @@ def in_bulk(self, id_list): return {} qs = self._clone() qs.query.add_filter(('pk__in', id_list)) + qs.query.clear_ordering(force_empty=True) return dict([(obj._get_pk_val(), obj) for obj in qs.iterator()]) def delete(self): @@ -620,8 +631,20 @@ def annotate(self, *args, **kwargs): with data aggregated from related fields. """ for arg in args: + if arg.default_alias in kwargs: + raise ValueError("The named annotation '%s' conflicts with the " + "default name for another annotation." + % arg.default_alias) kwargs[arg.default_alias] = arg + names = getattr(self, '_fields', None) + if names is None: + names = set(self.model._meta.get_all_field_names()) + for aggregate in kwargs: + if aggregate in names: + raise ValueError("The annotation '%s' conflicts with a field on " + "the model." % aggregate) + obj = self._clone() obj._setup_aggregate_query(kwargs.keys()) @@ -959,8 +982,7 @@ def iterator(self): # If a field list has been specified, use it. Otherwise, use the # full list of fields, including extras and aggregates. if self._fields: - fields = list(self._fields) + filter(lambda f: f not in self._fields, - aggregate_names) + fields = list(self._fields) + filter(lambda f: f not in self._fields, aggregate_names) else: fields = names @@ -970,7 +992,9 @@ def iterator(self): def _clone(self, *args, **kwargs): clone = super(ValuesListQuerySet, self)._clone(*args, **kwargs) - clone.flat = self.flat + if not hasattr(clone, "flat"): + # Only assign flat if the clone didn't already get it from kwargs + clone.flat = self.flat return clone @@ -1022,7 +1046,7 @@ def delete(self): pass def _clone(self, klass=None, setup=False, **kwargs): - c = super(EmptyQuerySet, self)._clone(klass, **kwargs) + c = super(EmptyQuerySet, self)._clone(klass, setup=setup, **kwargs) c._result_cache = [] return c @@ -1210,6 +1234,7 @@ def get_cached_row(klass, row, index_start, using, max_depth=0, cur_depth=0, # If an object was retrieved, set the database state. if obj: obj._state.db = using + obj._state.adding = False index_end = index_start + field_count + offset # Iterate over each related object, populating any @@ -1369,8 +1394,71 @@ def __init__(self, raw_query, model=None, query=None, params=None, self.translations = translations or {} def __iter__(self): - for row in self.query: - yield self.transform_results(row) + # Mapping of attrnames to row column positions. Used for constructing + # the model using kwargs, needed when not all model's fields are present + # in the query. + model_init_field_names = {} + # A list of tuples of (column name, column position). Used for + # annotation fields. + annotation_fields = [] + + # Cache some things for performance reasons outside the loop. + db = self.db + compiler = connections[db].ops.compiler('SQLCompiler')( + self.query, connections[db], db + ) + need_resolv_columns = hasattr(compiler, 'resolve_columns') + + query = iter(self.query) + + # Find out which columns are model's fields, and which ones should be + # annotated to the model. + for pos, column in enumerate(self.columns): + if column in self.model_fields: + model_init_field_names[self.model_fields[column].attname] = pos + else: + annotation_fields.append((column, pos)) + + # Find out which model's fields are not present in the query. + skip = set() + for field in self.model._meta.fields: + if field.attname not in model_init_field_names: + skip.add(field.attname) + if skip: + if self.model._meta.pk.attname in skip: + raise InvalidQuery('Raw query must include the primary key') + model_cls = deferred_class_factory(self.model, skip) + else: + model_cls = self.model + # All model's fields are present in the query. So, it is possible + # to use *args based model instantation. For each field of the model, + # record the query column position matching that field. + model_init_field_pos = [] + for field in self.model._meta.fields: + model_init_field_pos.append(model_init_field_names[field.attname]) + if need_resolv_columns: + fields = [self.model_fields.get(c, None) for c in self.columns] + # Begin looping through the query values. + for values in query: + if need_resolv_columns: + values = compiler.resolve_columns(values, fields) + # Associate fields to values + if skip: + model_init_kwargs = {} + for attname, pos in model_init_field_names.iteritems(): + model_init_kwargs[attname] = values[pos] + instance = model_cls(**model_init_kwargs) + else: + model_init_args = [values[pos] for pos in model_init_field_pos] + instance = model_cls(*model_init_args) + if annotation_fields: + for column, pos in annotation_fields: + setattr(instance, column, values[pos]) + + instance._state.db = db + instance._state.adding = False + + yield instance def __repr__(self): return "" % (self.raw_query % self.params) @@ -1425,49 +1513,6 @@ def model_fields(self): self._model_fields[converter(column)] = field return self._model_fields - def transform_results(self, values): - model_init_kwargs = {} - annotations = () - - # Perform database backend type resolution - connection = connections[self.db] - compiler = connection.ops.compiler('SQLCompiler')(self.query, connection, self.db) - if hasattr(compiler, 'resolve_columns'): - fields = [self.model_fields.get(c,None) for c in self.columns] - values = compiler.resolve_columns(values, fields) - - # Associate fields to values - for pos, value in enumerate(values): - column = self.columns[pos] - - # Separate properties from annotations - if column in self.model_fields.keys(): - model_init_kwargs[self.model_fields[column].attname] = value - else: - annotations += (column, value), - - # Construct model instance and apply annotations - skip = set() - for field in self.model._meta.fields: - if field.attname not in model_init_kwargs.keys(): - skip.add(field.attname) - - if skip: - if self.model._meta.pk.attname in skip: - raise InvalidQuery('Raw query must include the primary key') - model_cls = deferred_class_factory(self.model, skip) - else: - model_cls = self.model - - instance = model_cls(**model_init_kwargs) - - for field, value in annotations: - setattr(instance, field, value) - - instance._state.db = self.query.using - - return instance - def insert_query(model, values, return_id=False, raw_values=False, using=None): """ Inserts a new record for the given model. This provides an interface to diff --git a/django/db/models/sql/aggregates.py b/django/db/models/sql/aggregates.py index 8a14bdf2df4f..207bc0c6c86a 100644 --- a/django/db/models/sql/aggregates.py +++ b/django/db/models/sql/aggregates.py @@ -8,6 +8,7 @@ class AggregateField(object): """ def __init__(self, internal_type): self.internal_type = internal_type + def get_internal_type(self): return self.internal_type diff --git a/django/db/models/sql/compiler.py b/django/db/models/sql/compiler.py index eaf2cd256911..fb9674c366ed 100644 --- a/django/db/models/sql/compiler.py +++ b/django/db/models/sql/compiler.py @@ -327,7 +327,7 @@ def get_ordering(self): continue col, order = get_order_dir(field, asc) if col in self.query.aggregate_select: - result.append('%s %s' % (col, order)) + result.append('%s %s' % (qn(col), order)) continue if '.' in field: # This came in through an extra(order_by=...) addition. Pass it @@ -466,9 +466,11 @@ def get_grouping(self): qn = self.quote_name_unless_alias result, params = [], [] if self.query.group_by is not None: - if len(self.query.model._meta.fields) == len(self.query.select) and \ - self.connection.features.allows_group_by_pk: - self.query.group_by = [(self.query.model._meta.db_table, self.query.model._meta.pk.column)] + if (len(self.query.model._meta.fields) == len(self.query.select) and + self.connection.features.allows_group_by_pk): + self.query.group_by = [ + (self.query.model._meta.db_table, self.query.model._meta.pk.column) + ] group_by = self.query.group_by or [] @@ -476,11 +478,13 @@ def get_grouping(self): for extra_select, extra_params in self.query.extra_select.itervalues(): extra_selects.append(extra_select) params.extend(extra_params) - for col in group_by + self.query.related_select_cols + extra_selects: + cols = (group_by + self.query.select + + self.query.related_select_cols + extra_selects) + for col in cols: if isinstance(col, (list, tuple)): result.append('%s.%s' % (qn(col[0]), qn(col[1]))) elif hasattr(col, 'as_sql'): - result.append(col.as_sql(qn)) + result.append(col.as_sql(qn, self.connection)) else: result.append('(%s)' % str(col)) return result, params @@ -669,6 +673,7 @@ def results_iter(self): """ resolve_columns = hasattr(self, 'resolve_columns') fields = None + has_aggregate_select = bool(self.query.aggregate_select) for rows in self.execute_sql(MULTI): for row in rows: if resolve_columns: @@ -689,7 +694,7 @@ def results_iter(self): f.column in only_load[db_table]] row = self.resolve_columns(row, fields) - if self.query.aggregate_select: + if has_aggregate_select: aggregate_start = len(self.query.extra_select.keys()) + len(self.query.select) aggregate_end = aggregate_start + len(self.query.aggregate_select) row = tuple(row[:aggregate_start]) + tuple([ diff --git a/django/db/models/sql/expressions.py b/django/db/models/sql/expressions.py index 9bbc16ec8acf..fffbba085c33 100644 --- a/django/db/models/sql/expressions.py +++ b/django/db/models/sql/expressions.py @@ -19,7 +19,10 @@ def as_sql(self, qn, connection): def relabel_aliases(self, change_map): for node, col in self.cols.items(): - self.cols[node] = (change_map.get(col[0], col[0]), col[1]) + if hasattr(col, "relabel_aliases"): + col.relabel_aliases(change_map) + else: + self.cols[node] = (change_map.get(col[0], col[0]), col[1]) ##################################################### # Vistor methods for initial expression preparation # diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 0913399e2aa1..f50306632a67 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -195,8 +195,9 @@ def __setstate__(self, obj_dict): Unpickling support. """ # Rebuild list of field instances + opts = obj_dict['model']._meta obj_dict['select_fields'] = [ - name is not None and obj_dict['model']._meta.get_field(name) or None + name is not None and opts.get_field(name) or None for name in obj_dict['select_fields'] ] @@ -337,7 +338,7 @@ def get_aggregation(self, using): # information but retrieves only the first row. Aggregate # over the subquery instead. if self.group_by is not None: - from subqueries import AggregateQuery + from django.db.models.sql.subqueries import AggregateQuery query = AggregateQuery(self.model) obj = self.clone() @@ -349,7 +350,13 @@ def get_aggregation(self, using): query.aggregate_select[alias] = aggregate del obj.aggregate_select[alias] - query.add_subquery(obj, using) + try: + query.add_subquery(obj, using) + except EmptyResultSet: + return dict( + (alias, None) + for alias in query.aggregate_select + ) else: query = self self.select = [] @@ -382,13 +389,19 @@ def get_count(self, using): # If a select clause exists, then the query has already started to # specify the columns that are to be returned. # In this case, we need to use a subquery to evaluate the count. - from subqueries import AggregateQuery + from django.db.models.sql.subqueries import AggregateQuery subquery = obj subquery.clear_ordering(True) subquery.clear_limits() obj = AggregateQuery(obj.model) - obj.add_subquery(subquery, using=using) + try: + obj.add_subquery(subquery, using=using) + except EmptyResultSet: + # add_subquery evaluates the query, if it's an EmptyResultSet + # then there are can be no results, and therefore there the + # count is obviously 0 + return 0 obj.add_count_column() number = obj.get_aggregation(using=using)[None] @@ -433,6 +446,8 @@ def combine(self, rhs, connector): "Cannot combine a unique query with a non-unique query." self.remove_inherited_models() + l_tables = set([a for a in self.tables if self.alias_refcount[a]]) + r_tables = set([a for a in rhs.tables if rhs.alias_refcount[a]]) # Work out how to relabel the rhs aliases, if necessary. change_map = {} used = set() @@ -450,13 +465,19 @@ def combine(self, rhs, connector): first = False # So that we don't exclude valid results in an "or" query combination, - # the first join that is exclusive to the lhs (self) must be converted + # all joins exclusive to either the lhs or the rhs must be converted # to an outer join. if not conjunction: - for alias in self.tables[1:]: - if self.alias_refcount[alias] == 1: - self.promote_alias(alias, True) - break + # Update r_tables aliases. + for alias in change_map: + if alias in r_tables: + r_tables.remove(alias) + r_tables.add(change_map[alias]) + # Find aliases that are exclusive to rhs or lhs. + # These are promoted to outer joins. + outer_aliases = (l_tables | r_tables) - (l_tables & r_tables) + for alias in outer_aliases: + self.promote_alias(alias, True) # Now relabel a copy of the rhs where-clause and add it to the current # one. @@ -695,13 +716,20 @@ def change_aliases(self, change_map): # "group by", "where" and "having". self.where.relabel_aliases(change_map) self.having.relabel_aliases(change_map) - for columns in (self.select, self.aggregates.values(), self.group_by or []): + for columns in [self.select, self.group_by or []]: for pos, col in enumerate(columns): if isinstance(col, (list, tuple)): old_alias = col[0] columns[pos] = (change_map.get(old_alias, old_alias), col[1]) else: col.relabel_aliases(change_map) + for mapping in [self.aggregates]: + for key, col in mapping.items(): + if isinstance(col, (list, tuple)): + old_alias = col[0] + mapping[key] = (change_map.get(old_alias, old_alias), col[1]) + else: + col.relabel_aliases(change_map) # 2. Rename the alias in the internal table/alias datastructures. for old_alias, new_alias in change_map.iteritems(): @@ -902,6 +930,19 @@ def remove_inherited_models(self): self.unref_alias(alias) self.included_inherited_models = {} + def need_force_having(self, q_object): + """ + Returns whether or not all elements of this q_object need to be put + together in the HAVING clause. + """ + for child in q_object.children: + if isinstance(child, Node): + if self.need_force_having(child): + return True + else: + if child[0].split(LOOKUP_SEP)[0] in self.aggregates: + return True + return False def add_aggregate(self, aggregate, model, alias, is_summary): """ @@ -909,8 +950,7 @@ def add_aggregate(self, aggregate, model, alias, is_summary): """ opts = model._meta field_list = aggregate.lookup.split(LOOKUP_SEP) - if (len(field_list) == 1 and - aggregate.lookup in self.aggregates.keys()): + if len(field_list) == 1 and aggregate.lookup in self.aggregates: # Aggregate is over an annotation field_name = field_list[0] col = field_name @@ -953,7 +993,7 @@ def add_aggregate(self, aggregate, model, alias, is_summary): aggregate.add_to_query(self, alias, col=col, source=source, is_summary=is_summary) def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, - can_reuse=None, process_extras=True): + can_reuse=None, process_extras=True, force_having=False): """ Add a single filter to the query. The 'filter_expr' is a pair: (filter_string, value). E.g. ('name__contains', 'fred') @@ -1007,14 +1047,14 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, value = SQLEvaluator(value, self) having_clause = value.contains_aggregate - for alias, aggregate in self.aggregates.items(): - if alias == parts[0]: - entry = self.where_class() - entry.add((aggregate, lookup_type, value), AND) - if negate: - entry.negate() - self.having.add(entry, AND) - return + if parts[0] in self.aggregates: + aggregate = self.aggregates[parts[0]] + entry = self.where_class() + entry.add((aggregate, lookup_type, value), AND) + if negate: + entry.negate() + self.having.add(entry, connector) + return opts = self.get_meta() alias = self.get_initial_alias() @@ -1063,7 +1103,9 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, self.promote_alias_chain(table_it, table_promote) - if having_clause: + if having_clause or force_having: + if (alias, col) not in self.group_by: + self.group_by.append((alias, col)) self.having.add((Constraint(alias, col, field), lookup_type, value), connector) else: @@ -1078,11 +1120,14 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, if self.alias_map[alias][JOIN_TYPE] == self.LOUTER: j_col = self.alias_map[alias][RHS_JOIN_COL] entry = self.where_class() - entry.add((Constraint(alias, j_col, None), 'isnull', True), AND) + entry.add( + (Constraint(alias, j_col, None), 'isnull', True), + AND + ) entry.negate() self.where.add(entry, AND) break - elif not (lookup_type == 'in' + if not (lookup_type == 'in' and not hasattr(value, 'as_sql') and not hasattr(value, '_as_sql') and not value) and field.null: @@ -1090,10 +1135,7 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, # exclude the "foo__in=[]" case from this handling, because # it's short-circuited in the Where class. # We also need to handle the case where a subquery is provided - entry = self.where_class() - entry.add((Constraint(alias, col, None), 'isnull', True), AND) - entry.negate() - self.where.add(entry, AND) + self.where.add((Constraint(alias, col, None), 'isnull', False), AND) if can_reuse is not None: can_reuse.update(join_list) @@ -1102,7 +1144,7 @@ def add_filter(self, filter_expr, connector=AND, negate=False, trim=False, self.add_filter(filter, negate=negate, can_reuse=can_reuse, process_extras=False) - def add_q(self, q_object, used_aliases=None): + def add_q(self, q_object, used_aliases=None, force_having=False): """ Adds a Q-object to the current filter. @@ -1120,16 +1162,25 @@ def add_q(self, q_object, used_aliases=None): else: subtree = False connector = AND + if q_object.connector == OR and not force_having: + force_having = self.need_force_having(q_object) for child in q_object.children: if connector == OR: refcounts_before = self.alias_refcount.copy() - self.where.start_subtree(connector) + if force_having: + self.having.start_subtree(connector) + else: + self.where.start_subtree(connector) if isinstance(child, Node): - self.add_q(child, used_aliases) + self.add_q(child, used_aliases, force_having=force_having) else: self.add_filter(child, connector, q_object.negated, - can_reuse=used_aliases) - self.where.end_subtree() + can_reuse=used_aliases, force_having=force_having) + if force_having: + self.having.end_subtree() + else: + self.where.end_subtree() + if connector == OR: # Aliases that were newly added or not used at all need to # be promoted to outer joins if they are nullable relations. @@ -1240,12 +1291,14 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True, to_col2, opts, target) = cached_data else: table1 = field.m2m_db_table() - from_col1 = opts.pk.column + from_col1 = opts.get_field_by_name( + field.m2m_target_field_name())[0].column to_col1 = field.m2m_column_name() opts = field.rel.to._meta table2 = opts.db_table from_col2 = field.m2m_reverse_name() - to_col2 = opts.pk.column + to_col2 = opts.get_field_by_name( + field.m2m_reverse_target_field_name())[0].column target = opts.pk orig_opts._join_cache[name] = (table1, from_col1, to_col1, table2, from_col2, to_col2, opts, @@ -1293,12 +1346,14 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True, to_col2, opts, target) = cached_data else: table1 = field.m2m_db_table() - from_col1 = opts.pk.column + from_col1 = opts.get_field_by_name( + field.m2m_reverse_target_field_name())[0].column to_col1 = field.m2m_reverse_name() opts = orig_field.opts table2 = opts.db_table from_col2 = field.m2m_column_name() - to_col2 = opts.pk.column + to_col2 = opts.get_field_by_name( + field.m2m_target_field_name())[0].column target = opts.pk orig_opts._join_cache[name] = (table1, from_col1, to_col1, table2, from_col2, to_col2, opts, @@ -1322,7 +1377,12 @@ def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True, table = opts.db_table from_col = local_field.column to_col = field.column - target = opts.pk + # In case of a recursive FK, use the to_field for + # reverse lookups as well + if orig_field.model is local_field.model: + target = opts.get_field(field.rel.field_name) + else: + target = opts.pk orig_opts._join_cache[name] = (table, from_col, to_col, opts, target) @@ -1422,6 +1482,13 @@ def split_exclude(self, filter_expr, prefix, can_reuse): query.bump_prefix() query.clear_ordering(True) query.set_start(prefix) + # Adding extra check to make sure the selected field will not be null + # since we are adding a IN clause. This prevents the + # database from tripping over IN (...,NULL,...) selects and returning + # nothing + alias, col = query.select[0] + query.where.add((Constraint(alias, col, None), 'isnull', False), AND) + self.add_filter(('%s__in' % prefix, query), negate=True, trim=True, can_reuse=can_reuse) diff --git a/django/db/models/sql/subqueries.py b/django/db/models/sql/subqueries.py index a066dfeca8c7..a0bdc94a2cf8 100644 --- a/django/db/models/sql/subqueries.py +++ b/django/db/models/sql/subqueries.py @@ -10,6 +10,7 @@ from django.db.models.sql.query import Query from django.db.models.sql.where import AND, Constraint + __all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery', 'AggregateQuery'] diff --git a/django/db/models/sql/where.py b/django/db/models/sql/where.py index 4e5a6472594a..2427a528fb87 100644 --- a/django/db/models/sql/where.py +++ b/django/db/models/sql/where.py @@ -2,6 +2,7 @@ Code to manage the creation and SQL rendering of 'where' constraints. """ import datetime +from itertools import repeat from django.utils import tree from django.db.models.fields import Field @@ -36,10 +37,10 @@ class WhereNode(tree.Node): def add(self, data, connector): """ Add a node to the where-tree. If the data is a list or tuple, it is - expected to be of the form (alias, col_name, field_obj, lookup_type, - value), which is then slightly munged before being stored (to avoid - storing any reference to field objects). Otherwise, the 'data' is - stored unchanged and can be anything with an 'as_sql()' method. + expected to be of the form (obj, lookup_type, value), where obj is + a Constraint object, and is then slightly munged before being stored + (to avoid storing any reference to field objects). Otherwise, the 'data' + is stored unchanged and can be any class with an 'as_sql()' method. """ if not isinstance(data, (list, tuple)): super(WhereNode, self).add(data, connector) @@ -178,8 +179,24 @@ def make_atom(self, child, qn, connection): raise EmptyResultSet if extra: return ('%s IN %s' % (field_sql, extra), params) - return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(params))), - params) + max_in_list_size = connection.ops.max_in_list_size() + if max_in_list_size and len(params) > max_in_list_size: + # Break up the params list into an OR of manageable chunks. + in_clause_elements = ['('] + for offset in xrange(0, len(params), max_in_list_size): + if offset > 0: + in_clause_elements.append(' OR ') + in_clause_elements.append('%s IN (' % field_sql) + group_size = min(len(params) - offset, max_in_list_size) + param_group = ', '.join(repeat('%s', group_size)) + in_clause_elements.append(param_group) + in_clause_elements.append(')') + in_clause_elements.append(')') + return ''.join(in_clause_elements), params + else: + return ('%s IN (%s)' % (field_sql, + ', '.join(repeat('%s', len(params)))), + params) elif lookup_type in ('range', 'year'): return ('%s BETWEEN %%s and %%s' % field_sql, params) elif lookup_type in ('month', 'day', 'week_day'): diff --git a/django/db/transaction.py b/django/db/transaction.py index 3c767f1ae061..af42fd5493fe 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -288,7 +288,7 @@ def commit_on_success(using=None): This decorator activates commit on response. This way, if the view function runs successfully, a commit is made; if the viewfunc produces an exception, a rollback is made. This is one of the most common ways to do transaction - control in web apps. + control in Web apps. """ def inner_commit_on_success(func, db=None): def _commit_on_success(*args, **kw): diff --git a/django/db/utils.py b/django/db/utils.py index 00b3568cb065..8ae84762c8aa 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -5,6 +5,7 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module + DEFAULT_DB_ALIAS = 'default' # Define some exceptions that mirror the PEP249 interface. @@ -111,9 +112,11 @@ def __init__(self, routers): except ImportError, e: raise ImproperlyConfigured('Error importing database router %s: "%s"' % (klass_name, e)) try: - router = getattr(module, klass_name)() + router_class = getattr(module, klass_name) except AttributeError: raise ImproperlyConfigured('Module "%s" does not define a database router name "%s"' % (module, klass_name)) + else: + router = router_class() else: router = r self.routers.append(router) @@ -123,12 +126,14 @@ def _route_db(self, model, **hints): chosen_db = None for router in self.routers: try: - chosen_db = getattr(router, action)(model, **hints) - if chosen_db: - return chosen_db + method = getattr(router, action) except AttributeError: # If the router doesn't have a method, skip to the next one. pass + else: + chosen_db = method(model, **hints) + if chosen_db: + return chosen_db try: return hints['instance']._state.db or DEFAULT_DB_ALIAS except KeyError: @@ -141,21 +146,25 @@ def _route_db(self, model, **hints): def allow_relation(self, obj1, obj2, **hints): for router in self.routers: try: - allow = router.allow_relation(obj1, obj2, **hints) - if allow is not None: - return allow + method = router.allow_relation except AttributeError: # If the router doesn't have a method, skip to the next one. pass + else: + allow = method(obj1, obj2, **hints) + if allow is not None: + return allow return obj1._state.db == obj2._state.db def allow_syncdb(self, db, model): for router in self.routers: try: - allow = router.allow_syncdb(db, model) - if allow is not None: - return allow + method = router.allow_syncdb except AttributeError: # If the router doesn't have a method, skip to the next one. pass + else: + allow = method(db, model) + if allow is not None: + return allow return True diff --git a/django/dispatch/dispatcher.py b/django/dispatch/dispatcher.py index de093346375b..c0301cd7b896 100644 --- a/django/dispatch/dispatcher.py +++ b/django/dispatch/dispatcher.py @@ -1,4 +1,5 @@ import weakref +import threading from django.dispatch import saferef @@ -30,6 +31,7 @@ def __init__(self, providing_args=None): if providing_args is None: providing_args = [] self.providing_args = set(providing_args) + self.lock = threading.Lock() def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): """ @@ -41,7 +43,7 @@ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): A function or an instance method which is to receive signals. Receivers must be hashable objects. - if weak is True, then receiver must be weak-referencable (more + If weak is True, then receiver must be weak-referencable (more precisely saferef.safeRef() must be able to create a reference to the receiver). @@ -52,11 +54,11 @@ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): dispatch_uid. sender - The sender to which the receiver should respond Must either be + The sender to which the receiver should respond. Must either be of type Signal, or None to receive events from any sender. weak - Whether to use weak references to the receiver By default, the + Whether to use weak references to the receiver. By default, the module will attempt to use weak references to the receiver objects. If this parameter is false, then strong references will be used. @@ -97,11 +99,15 @@ def connect(self, receiver, sender=None, weak=True, dispatch_uid=None): if weak: receiver = saferef.safeRef(receiver, onDelete=self._remove_receiver) - for r_key, _ in self.receivers: - if r_key == lookup_key: - break - else: - self.receivers.append((lookup_key, receiver)) + self.lock.acquire() + try: + for r_key, _ in self.receivers: + if r_key == lookup_key: + break + else: + self.receivers.append((lookup_key, receiver)) + finally: + self.lock.release() def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None): """ @@ -130,11 +136,15 @@ def disconnect(self, receiver=None, sender=None, weak=True, dispatch_uid=None): else: lookup_key = (_make_id(receiver), _make_id(sender)) - for index in xrange(len(self.receivers)): - (r_key, _) = self.receivers[index] - if r_key == lookup_key: - del self.receivers[index] - break + self.lock.acquire() + try: + for index in xrange(len(self.receivers)): + (r_key, _) = self.receivers[index] + if r_key == lookup_key: + del self.receivers[index] + break + finally: + self.lock.release() def send(self, sender, **named): """ @@ -170,7 +180,7 @@ def send_robust(self, sender, **named): Arguments: sender - The sender of the signal Can be any python object (normally one + The sender of the signal. Can be any python object (normally one registered with a connect if you actually want something to occur). @@ -182,7 +192,7 @@ def send_robust(self, sender, **named): Return a list of tuple pairs [(receiver, response), ... ]. May raise DispatcherKeyError. - if any receiver raises an error (specifically any subclass of + If any receiver raises an error (specifically any subclass of Exception), the error instance is returned as the result for that receiver. """ @@ -227,11 +237,20 @@ def _remove_receiver(self, receiver): Remove dead receivers from connections. """ - to_remove = [] - for key, connected_receiver in self.receivers: - if connected_receiver == receiver: - to_remove.append(key) - for key in to_remove: - for idx, (r_key, _) in enumerate(self.receivers): - if r_key == key: - del self.receivers[idx] + self.lock.acquire() + try: + to_remove = [] + for key, connected_receiver in self.receivers: + if connected_receiver == receiver: + to_remove.append(key) + for key in to_remove: + last_idx = len(self.receivers) - 1 + # enumerate in reverse order so that indexes are valid even + # after we delete some items + for idx, (r_key, _) in enumerate(reversed(self.receivers)): + if r_key == key: + del self.receivers[last_idx-idx] + finally: + self.lock.release() + + diff --git a/django/forms/extras/widgets.py b/django/forms/extras/widgets.py index 7d05942f43e0..9525957d6ded 100644 --- a/django/forms/extras/widgets.py +++ b/django/forms/extras/widgets.py @@ -17,6 +17,26 @@ RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') +def _parse_date_fmt(): + fmt = get_format('DATE_FORMAT') + escaped = False + output = [] + for char in fmt: + if escaped: + escaped = False + elif char == '\\': + escaped = True + elif char in 'Yy': + output.append('year') + #if not self.first_select: self.first_select = 'year' + elif char in 'bFMmNn': + output.append('month') + #if not self.first_select: self.first_select = 'month' + elif char in 'dj': + output.append('day') + #if not self.first_select: self.first_select = 'day' + return output + class SelectDateWidget(Widget): """ A Widget that splits date input into three ') return mark_safe(u'\n'.join(output)) + def render_option(self, selected_choices, option_value, option_label): + option_value = force_unicode(option_value) + selected_html = (option_value in selected_choices) and u' selected="selected"' or '' + return u'' % ( + escape(option_value), selected_html, + conditional_escape(force_unicode(option_label))) + def render_options(self, choices, selected_choices): - def render_option(option_value, option_label): - option_value = force_unicode(option_value) - selected_html = (option_value in selected_choices) and u' selected="selected"' or '' - return u'' % ( - escape(option_value), selected_html, - conditional_escape(force_unicode(option_label))) # Normalize to strings. selected_choices = set([force_unicode(v) for v in selected_choices]) output = [] @@ -452,10 +465,10 @@ def render_option(option_value, option_label): if isinstance(option_label, (list, tuple)): output.append(u'' % escape(force_unicode(option_value))) for option in option_label: - output.append(render_option(*option)) + output.append(self.render_option(selected_choices, *option)) output.append(u'') else: - output.append(render_option(option_value, option_label)) + output.append(self.render_option(selected_choices, option_value, option_label)) return u'\n'.join(output) class NullBooleanSelect(Select): @@ -514,10 +527,9 @@ def _has_changed(self, initial, data): data = [] if len(initial) != len(data): return True - for value1, value2 in zip(initial, data): - if force_unicode(value1) != force_unicode(value2): - return True - return False + initial_set = set([force_unicode(value) for value in initial]) + data_set = set([force_unicode(value) for value in data]) + return data_set != initial_set class RadioInput(StrAndUnicode): """ @@ -751,12 +763,8 @@ class SplitDateTimeWidget(MultiWidget): time_format = TimeInput.format def __init__(self, attrs=None, date_format=None, time_format=None): - if date_format: - self.date_format = date_format - if time_format: - self.time_format = time_format - widgets = (DateInput(attrs=attrs, format=self.date_format), - TimeInput(attrs=attrs, format=self.time_format)) + widgets = (DateInput(attrs=attrs, format=date_format), + TimeInput(attrs=attrs, format=time_format)) super(SplitDateTimeWidget, self).__init__(widgets, attrs) def decompress(self, value): diff --git a/django/http/__init__.py b/django/http/__init__.py index c3917a16b22d..4a5ba3e39f67 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -45,7 +45,8 @@ def __repr__(self): def get_host(self): """Returns the HTTP host using the environment or request headers.""" # We try three options, in order of decreasing preference. - if 'HTTP_X_FORWARDED_HOST' in self.META: + if settings.USE_X_FORWARDED_HOST and ( + 'HTTP_X_FORWARDED_HOST' in self.META): host = self.META['HTTP_X_FORWARDED_HOST'] elif 'HTTP_HOST' in self.META: host = self.META['HTTP_HOST'] @@ -276,13 +277,33 @@ def value_encode(self, val): return val, encoded + def load(self, rawdata, ignore_parse_errors=False): + if ignore_parse_errors: + self.bad_cookies = [] + self._BaseCookie__set = self._loose_set + SimpleCookie.load(self, rawdata) + if ignore_parse_errors: + self._BaseCookie__set = self._strict_set + for key in self.bad_cookies: + del self[key] + + _strict_set = BaseCookie._BaseCookie__set + + def _loose_set(self, key, real_value, coded_value): + try: + self._strict_set(key, real_value, coded_value) + except CookieError: + self.bad_cookies.append(key) + dict.__setitem__(self, key, None) + + def parse_cookie(cookie): if cookie == '': return {} if not isinstance(cookie, BaseCookie): try: c = CompatCookie() - c.load(cookie) + c.load(cookie, ignore_parse_errors=True) except CookieError: # Invalid cookie return {} @@ -303,13 +324,16 @@ class HttpResponse(object): def __init__(self, content='', mimetype=None, status=None, content_type=None): - from django.conf import settings + # _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. + self._headers = {} self._charset = settings.DEFAULT_CHARSET if mimetype: content_type = mimetype # For backwards compatibility if not content_type: content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, - settings.DEFAULT_CHARSET) + self._charset) if not isinstance(content, basestring) and hasattr(content, '__iter__'): self._container = content self._is_string = False @@ -320,10 +344,7 @@ def __init__(self, content='', mimetype=None, status=None, if status: self.status_code = status - # _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. - self._headers = {'content-type': ('Content-Type', content_type)} + self['Content-Type'] = content_type def __str__(self): """Full HTTP message, including headers.""" diff --git a/django/http/utils.py b/django/http/utils.py index 4dc05a2e3336..5eea23907b0e 100644 --- a/django/http/utils.py +++ b/django/http/utils.py @@ -38,7 +38,8 @@ def fix_IE_for_attach(request, response): while expecting the browser to cache it (only when the browser is IE). This leads to IE not allowing the client to download. """ - if 'MSIE' not in request.META.get('HTTP_USER_AGENT', '').upper(): + useragent = request.META.get('HTTP_USER_AGENT', '').upper() + if 'MSIE' not in useragent and 'CHROMEFRAME' not in useragent: return response offending_headers = ('no-cache', 'no-store') @@ -66,7 +67,8 @@ def fix_IE_for_vary(request, response): by clearing the Vary header whenever the mime-type is not safe enough for Internet Explorer to handle. Poor thing. """ - if 'MSIE' not in request.META.get('HTTP_USER_AGENT', '').upper(): + useragent = request.META.get('HTTP_USER_AGENT', '').upper() + if 'MSIE' not in useragent and 'CHROMEFRAME' not in useragent: return response # These mime-types that are decreed "Vary-safe" for IE: diff --git a/django/middleware/cache.py b/django/middleware/cache.py index 3f602fe652e0..f70e76794466 100644 --- a/django/middleware/cache.py +++ b/django/middleware/cache.py @@ -52,6 +52,7 @@ from django.core.cache import cache from django.utils.cache import get_cache_key, learn_cache_key, patch_response_headers, get_max_age + class UpdateCacheMiddleware(object): """ Response-phase cache middleware that updates the cache if the response is @@ -66,9 +67,28 @@ def __init__(self): self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX self.cache_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) + def _session_accessed(self, request): + try: + return request.session.accessed + except AttributeError: + return False + + def _should_update_cache(self, request, response): + if not hasattr(request, '_cache_update_cache') or not request._cache_update_cache: + return False + # If the session has not been accessed otherwise, we don't want to + # cause it to be accessed here. If it hasn't been accessed, then the + # user's logged-in status has not affected the response anyway. + if self.cache_anonymous_only and self._session_accessed(request): + assert hasattr(request, 'user'), "The Django cache middleware with CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True requires authentication middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.auth.middleware.AuthenticationMiddleware' before the CacheMiddleware." + if request.user.is_authenticated(): + # Don't cache user-variable requests from authenticated users. + return False + return True + def process_response(self, request, response): """Sets the cache, if needed.""" - if not hasattr(request, '_cache_update_cache') or not request._cache_update_cache: + if not self._should_update_cache(request, response): # We don't need to update the cache, just return. return response if request.method != 'GET': @@ -112,17 +132,10 @@ def process_request(self, request): Checks whether the page is already cached and returns the cached version if available. """ - if self.cache_anonymous_only: - assert hasattr(request, 'user'), "The Django cache middleware with CACHE_MIDDLEWARE_ANONYMOUS_ONLY=True requires authentication middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.auth.middleware.AuthenticationMiddleware' before the CacheMiddleware." - if not request.method in ('GET', 'HEAD') or request.GET: request._cache_update_cache = False return None # Don't bother checking the cache. - if self.cache_anonymous_only and request.user.is_authenticated(): - request._cache_update_cache = False - return None # Don't cache requests from authenticated users. - cache_key = get_cache_key(request, self.key_prefix) if cache_key is None: request._cache_update_cache = True diff --git a/django/middleware/common.py b/django/middleware/common.py index 309058870a2a..20619d5d22fe 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -80,9 +80,9 @@ def process_request(self, request): return http.HttpResponsePermanentRedirect(newurl) def process_response(self, request, response): - "Check for a flat page (for 404s) and calculate the Etag, if needed." + "Send broken link emails and calculate the Etag, if needed." if response.status_code == 404: - if settings.SEND_BROKEN_LINK_EMAILS: + if settings.SEND_BROKEN_LINK_EMAILS and not settings.DEBUG: # If the referrer was from an internal link or a non-search-engine site, # send a note to the managers. domain = request.get_host() @@ -94,7 +94,8 @@ def process_response(self, request, response): ip = request.META.get('REMOTE_ADDR', '') mail_managers("Broken %slink on %s" % ((is_internal and 'INTERNAL ' or ''), domain), "Referrer: %s\nRequested URL: %s\nUser agent: %s\nIP address: %s\n" \ - % (referer, request.get_full_path(), ua, ip)) + % (referer, request.get_full_path(), ua, ip), + fail_silently=True) return response # Use ETags, if requested. diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index 10fab290c9a9..b73fa95bb727 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -13,6 +13,7 @@ from django.core.urlresolvers import get_callable from django.utils.cache import patch_vary_headers from django.utils.hashcompat import md5_constructor +from django.utils.http import same_origin from django.utils.safestring import mark_safe _POST_FORM_RE = \ @@ -27,22 +28,33 @@ randrange = random.randrange _MAX_CSRF_KEY = 18446744073709551616L # 2 << 63 +REASON_NO_REFERER = "Referer checking failed - no Referer." +REASON_BAD_REFERER = "Referer checking failed - %s does not match %s." +REASON_NO_COOKIE = "No CSRF or session cookie." +REASON_NO_CSRF_COOKIE = "CSRF cookie not set." +REASON_BAD_TOKEN = "CSRF token missing or incorrect." + + def _get_failure_view(): """ Returns the view to be used for CSRF rejections """ return get_callable(settings.CSRF_FAILURE_VIEW) + def _get_new_csrf_key(): return md5_constructor("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() + def _make_legacy_session_token(session_id): return md5_constructor(settings.SECRET_KEY + session_id).hexdigest() + def get_token(request): """ - Returns the the CSRF token required for a POST form. + Returns the the CSRF token required for a POST form. The token is an + alphanumeric value. A side effect of calling this function is to make the the csrf_protect decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' @@ -52,6 +64,18 @@ def get_token(request): request.META["CSRF_COOKIE_USED"] = True return request.META.get("CSRF_COOKIE", None) + +def _sanitize_token(token): + # Allow only alphanum, and ensure we return a 'str' for the sake of the post + # processing middleware. + token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore'))) + if token == "": + # In case the cookie has been truncated to nothing at some point. + return _get_new_csrf_key() + else: + return token + + class CsrfViewMiddleware(object): """ Middleware that requires a present and correct csrfmiddlewaretoken @@ -61,23 +85,31 @@ class CsrfViewMiddleware(object): This middleware should be used in conjunction with the csrf_token template tag. """ + # The _accept and _reject methods currently only exist for the sake of the + # requires_csrf_token decorator. + def _accept(self, request): + # Avoid checking the request twice by adding a custom attribute to + # request. This will be relevant when both decorator and middleware + # are used. + request.csrf_processing_done = True + return None + + def _reject(self, request, reason): + return _get_failure_view()(request, reason=reason) + def process_view(self, request, callback, callback_args, callback_kwargs): - if getattr(request, 'csrf_processing_done', False): - return None - reject = lambda s: _get_failure_view()(request, reason=s) - def accept(): - # Avoid checking the request twice by adding a custom attribute to - # request. This will be relevant when both decorator and middleware - # are used. - request.csrf_processing_done = True + if getattr(request, 'csrf_processing_done', False): return None # If the user doesn't have a CSRF cookie, generate one and store it in the # request, so it's available to the view. We'll store it in a cookie when # we reach the response. try: - request.META["CSRF_COOKIE"] = request.COOKIES[settings.CSRF_COOKIE_NAME] + # In case of cookies from untrusted sources, we strip anything + # dangerous at this point, so that the cookie + token will have the + # same, sanitized value. + request.META["CSRF_COOKIE"] = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME]) cookie_is_new = False except KeyError: # No cookie, so create one. This will be sent with the next @@ -98,45 +130,19 @@ def accept(): # the creation of CSRF cookies, so that everything else continues to # work exactly the same (e.g. cookies are sent etc), but before the # any branches that call reject() - return accept() - - if request.is_ajax(): - # .is_ajax() is based on the presence of X-Requested-With. In - # the context of a browser, this can only be sent if using - # XmlHttpRequest. Browsers implement careful policies for - # XmlHttpRequest: - # - # * Normally, only same-domain requests are allowed. - # - # * Some browsers (e.g. Firefox 3.5 and later) relax this - # carefully: - # - # * if it is a 'simple' GET or POST request (which can - # include no custom headers), it is allowed to be cross - # domain. These requests will not be recognized as AJAX. - # - # * if a 'preflight' check with the server confirms that the - # server is expecting and allows the request, cross domain - # requests even with custom headers are allowed. These - # requests will be recognized as AJAX, but can only get - # through when the developer has specifically opted in to - # allowing the cross-domain POST request. - # - # So in all cases, it is safe to allow these requests through. - return accept() + return self._accept(request) if request.is_secure(): # Strict referer checking for HTTPS referer = request.META.get('HTTP_REFERER') if referer is None: - return reject("Referer checking failed - no Referer.") + return self._reject(request, REASON_NO_REFERER) - # The following check ensures that the referer is HTTPS, - # the domains match and the ports match. This might be too strict. + # Note that request.get_host() includes the port good_referer = 'https://%s/' % request.get_host() - if not referer.startswith(good_referer): - return reject("Referer checking failed - %s does not match %s." % - (referer, good_referer)) + if not same_origin(referer, good_referer): + return self._reject(request, REASON_BAD_REFERER % + (referer, good_referer)) # If the user didn't already have a CSRF cookie, then fall back to # the Django 1.1 method (hash of session ID), so a request is not @@ -150,20 +156,24 @@ def accept(): # No CSRF cookie and no session cookie. For POST requests, # we insist on a CSRF cookie, and in this way we can avoid # all CSRF attacks, including login CSRF. - return reject("No CSRF or session cookie.") + return self._reject(request, REASON_NO_COOKIE) else: csrf_token = request.META["CSRF_COOKIE"] # check incoming token - request_csrf_token = request.POST.get('csrfmiddlewaretoken', None) + request_csrf_token = request.POST.get('csrfmiddlewaretoken', "") + if request_csrf_token == "": + # Fall back to X-CSRFToken, to make things easier for AJAX + request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '') + if request_csrf_token != csrf_token: if cookie_is_new: # probably a problem setting the CSRF cookie - return reject("CSRF cookie not set.") + return self._reject(request, REASON_NO_CSRF_COOKIE) else: - return reject("CSRF token missing or incorrect.") + return self._reject(request, REASON_BAD_TOKEN) - return accept() + return self._accept(request) def process_response(self, request, response): if getattr(response, 'csrf_processing_done', False): @@ -187,6 +197,7 @@ def process_response(self, request, response): response.csrf_processing_done = True return response + class CsrfResponseMiddleware(object): """ DEPRECATED @@ -237,6 +248,7 @@ def add_csrf_field(match): del response['ETag'] return response + class CsrfMiddleware(object): """ Django middleware that adds protection against Cross Site @@ -264,4 +276,3 @@ def process_response(self, request, resp): def process_view(self, request, callback, callback_args, callback_kwargs): return self.view_middleware.process_view(request, callback, callback_args, callback_kwargs) - diff --git a/django/middleware/http.py b/django/middleware/http.py index 75af664447f5..9269b65c94ee 100644 --- a/django/middleware/http.py +++ b/django/middleware/http.py @@ -1,5 +1,5 @@ from django.core.exceptions import MiddlewareNotUsed -from django.utils.http import http_date +from django.utils.http import http_date, parse_http_date_safe class ConditionalGetMiddleware(object): """ @@ -15,7 +15,7 @@ def process_response(self, request, response): response['Content-Length'] = str(len(response.content)) if response.has_header('ETag'): - if_none_match = request.META.get('HTTP_IF_NONE_MATCH', None) + if_none_match = request.META.get('HTTP_IF_NONE_MATCH') if if_none_match == response['ETag']: # Setting the status is enough here. The response handling path # automatically removes content for this status code (in @@ -23,11 +23,15 @@ def process_response(self, request, response): response.status_code = 304 if response.has_header('Last-Modified'): - if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE', None) - if if_modified_since == response['Last-Modified']: - # Setting the status code is enough here (same reasons as - # above). - response.status_code = 304 + if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE') + if if_modified_since is not None: + if_modified_since = parse_http_date_safe(if_modified_since) + if if_modified_since is not None: + last_modified = parse_http_date_safe(response['Last-Modified']) + if last_modified is not None and last_modified <= if_modified_since: + # Setting the status code is enough here (same reasons as + # above). + response.status_code = 304 return response diff --git a/django/template/__init__.py b/django/template/__init__.py index c3167861fdeb..8c89c67b23b1 100644 --- a/django/template/__init__.py +++ b/django/template/__init__.py @@ -59,7 +59,7 @@ from django.utils.functional import curry, Promise from django.utils.text import smart_split, unescape_string_literal, get_text_list from django.utils.encoding import smart_unicode, force_unicode, smart_str -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping from django.utils.formats import localize from django.utils.html import escape @@ -698,7 +698,7 @@ def resolve(self, context): # We're dealing with a literal, so it's already been "resolved" value = self.literal if self.translate: - return _(value) + return ugettext_lazy(value) return value def __repr__(self): diff --git a/django/template/context.py b/django/template/context.py index 323c1446b2f7..bc12cd6fbf31 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -1,5 +1,7 @@ +from copy import copy from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module +from django.http import HttpRequest # Cache of actual callables. _standard_context_processors = None @@ -12,11 +14,23 @@ class ContextPopException(Exception): "pop() has been called more times than push()" pass +class EmptyClass(object): + # No-op class which takes no args to its __init__ method, to help implement + # __copy__ + pass + class BaseContext(object): def __init__(self, dict_=None): dict_ = dict_ or {} self.dicts = [dict_] + def __copy__(self): + duplicate = EmptyClass() + duplicate.__class__ = self.__class__ + duplicate.__dict__ = self.__dict__.copy() + duplicate.dicts = duplicate.dicts[:] + return duplicate + def __repr__(self): return repr(self.dicts) @@ -72,6 +86,11 @@ def __init__(self, dict_=None, autoescape=True, current_app=None): self.render_context = RenderContext() super(Context, self).__init__(dict_) + def __copy__(self): + duplicate = super(Context, self).__copy__() + duplicate.render_context = copy(self.render_context) + return duplicate + def update(self, other_dict): "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." if not hasattr(other_dict, '__getitem__'): diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index 4b720093b32f..5902ebd301b5 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -11,9 +11,10 @@ from django.template import Variable, Library from django.conf import settings from django.utils import formats -from django.utils.translation import ugettext, ungettext from django.utils.encoding import force_unicode, iri_to_uri +from django.utils.html import conditional_escape from django.utils.safestring import mark_safe, SafeData +from django.utils.translation import ugettext, ungettext register = Library() @@ -63,29 +64,10 @@ def capfirst(value): capfirst.is_safe=True capfirst = stringfilter(capfirst) -_base_js_escapes = ( - ('\\', r'\u005C'), - ('\'', r'\u0027'), - ('"', r'\u0022'), - ('>', r'\u003E'), - ('<', r'\u003C'), - ('&', r'\u0026'), - ('=', r'\u003D'), - ('-', r'\u002D'), - (';', r'\u003B'), - (u'\u2028', r'\u2028'), - (u'\u2029', r'\u2029') -) - -# Escape every ASCII character with a value less than 32. -_js_escapes = (_base_js_escapes + - tuple([('%c' % z, '\\u%04X' % z) for z in range(32)])) - def escapejs(value): """Hex encodes characters for use in JavaScript strings.""" - for bad, good in _js_escapes: - value = value.replace(bad, good) - return value + from django.utils.html import escapejs + return escapejs(value) escapejs = stringfilter(escapejs) def fix_ampersands(value): @@ -167,9 +149,19 @@ def floatformat(text, arg=-1): if p == 0: exp = Decimal(1) else: - exp = Decimal('1.0') / (Decimal(10) ** abs(p)) + exp = Decimal(u'1.0') / (Decimal(10) ** abs(p)) try: - return mark_safe(formats.number_format(u'%s' % str(d.quantize(exp, ROUND_HALF_UP)), abs(p))) + # Avoid conversion to scientific notation by accessing `sign`, `digits` + # and `exponent` from `Decimal.as_tuple()` directly. + sign, digits, exponent = d.quantize(exp, ROUND_HALF_UP).as_tuple() + digits = [unicode(digit) for digit in reversed(digits)] + while len(digits) <= abs(exponent): + digits.append(u'0') + digits.insert(-exponent, u'.') + if sign: + digits.append(u'-') + number = u''.join(reversed(digits)) + return mark_safe(formats.number_format(number, abs(p))) except InvalidOperation: return input_val floatformat.is_safe = True @@ -255,6 +247,8 @@ def truncatewords(value, arg): Truncates a string after a certain number of words. Argument: Number of words to truncate after. + + Newlines within the string are removed. """ from django.utils.text import truncate_words try: @@ -270,6 +264,8 @@ def truncatewords_html(value, arg): Truncates HTML after a certain number of words. Argument: Number of words to truncate after. + + Newlines in the HTML are preserved. """ from django.utils.text import truncate_html_words try: @@ -496,10 +492,9 @@ def join(value, arg, autoescape=None): """ value = map(force_unicode, value) if autoescape: - from django.utils.html import conditional_escape value = [conditional_escape(v) for v in value] try: - data = arg.join(value) + data = conditional_escape(arg).join(value) except AttributeError: # fail silently but nicely return value return mark_safe(data) @@ -597,6 +592,10 @@ def convert_old_style_list(list_): first_item, second_item = list_ if second_item == []: return [first_item], True + try: + it = iter(second_item) # see if second item is iterable + except TypeError: + return list_, False old_style_list = True new_second_item = [] for sublist in second_item: @@ -727,7 +726,6 @@ def timesince(value, arg=None): def timeuntil(value, arg=None): """Formats a date as the time until that date (i.e. "4 days, 6 hours").""" from django.utils.timesince import timeuntil - from datetime import datetime if not value: return u'' try: @@ -801,15 +799,17 @@ def filesizeformat(bytes): try: bytes = float(bytes) except (TypeError,ValueError,UnicodeDecodeError): - return u"0 bytes" + return ungettext("%(size)d byte", "%(size)d bytes", 0) % {'size': 0} + + filesize_number_format = lambda value: formats.number_format(round(value, 1), 1) if bytes < 1024: return ungettext("%(size)d byte", "%(size)d bytes", bytes) % {'size': bytes} if bytes < 1024 * 1024: - return ugettext("%.1f KB") % (bytes / 1024) + return ugettext("%s KB") % filesize_number_format(bytes / 1024) if bytes < 1024 * 1024 * 1024: - return ugettext("%.1f MB") % (bytes / (1024 * 1024)) - return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024)) + return ugettext("%s MB") % filesize_number_format(bytes / (1024 * 1024)) + return ugettext("%s GB") % filesize_number_format(bytes / (1024 * 1024 * 1024)) filesizeformat.is_safe = True def pluralize(value, arg=u's'): diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 318ae5ffd2ee..1b0741353037 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -42,7 +42,7 @@ def render(self, context): if csrf_token == 'NOTPROVIDED': return mark_safe(u"") else: - return mark_safe(u"
          " % (csrf_token)) + return mark_safe(u"
          " % csrf_token) else: # It's very probable that the token is missing because of # misconfiguration, so we raise a warning @@ -157,15 +157,22 @@ def render(self, context): loop_dict['first'] = (i == 0) loop_dict['last'] = (i == len_values - 1) + pop_context = False if unpack: # If there are multiple loop variables, unpack the item into # them. - context.update(dict(zip(self.loopvars, item))) + try: + unpacked_vars = dict(zip(self.loopvars, item)) + except TypeError: + pass + else: + pop_context = True + context.update(unpacked_vars) else: context[self.loopvars[0]] = item for node in self.nodelist_loop: nodelist.append(node.render(context)) - if unpack: + if pop_context: # The loop variables were pushed on to the context so pop them # off again. This is necessary because the tag lets the length # of loopvars differ to the length of each set of items and we diff --git a/django/template/loader.py b/django/template/loader.py index b8077522fc57..e9ccd1a49975 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -141,12 +141,12 @@ def find_template_source(name, dirs=None): # For backward compatibility import warnings warnings.warn( - "`django.template.loaders.find_template_source` is deprecated; use `django.template.loaders.find_template` instead.", + "`django.template.loaders.find_template_source` is deprecated; use `django.template.loader.find_template` instead.", PendingDeprecationWarning ) template, origin = find_template(name, dirs) if hasattr(template, 'render'): - raise Exception("Found a compiled template that is incompatible with the deprecated `django.template.loaders.find_template_source` function.") + raise Exception("Found a compiled template that is incompatible with the deprecated `django.template.loader.find_template_source` function.") return template, origin def get_template(template_name): @@ -179,20 +179,27 @@ def render_to_string(template_name, dictionary=None, context_instance=None): t = select_template(template_name) else: t = get_template(template_name) - if context_instance: - context_instance.update(dictionary) - else: - context_instance = Context(dictionary) - return t.render(context_instance) + if not context_instance: + return t.render(Context(dictionary)) + # Add the dictionary to the context stack, ensuring it gets removed again + # to keep the context_instance in the same state it started in. + context_instance.update(dictionary) + try: + return t.render(context_instance) + finally: + context_instance.pop() def select_template(template_name_list): "Given a list of template names, returns the first that can be loaded." + not_found = [] for template_name in template_name_list: try: return get_template(template_name) - except TemplateDoesNotExist: + except TemplateDoesNotExist, e: + if e.args[0] not in not_found: + not_found.append(e.args[0]) continue # If we get here, none of the templates could be loaded - raise TemplateDoesNotExist(', '.join(template_name_list)) + raise TemplateDoesNotExist(', '.join(not_found)) add_to_builtins('django.template.loader_tags') diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py index b8bc741d4117..ba06805cb6c8 100644 --- a/django/template/loader_tags.py +++ b/django/template/loader_tags.py @@ -149,12 +149,10 @@ def render(self, context): template_name = self.template_name.resolve(context) t = get_template(template_name) return t.render(context) - except TemplateSyntaxError, e: + except: if settings.TEMPLATE_DEBUG: raise return '' - except: - return '' # Fail silently for invalid included templates. def do_block(parser, token): """ diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 10ac900041db..3189fce001ae 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -76,13 +76,14 @@ def render(self, context): if self.plural and self.countervar and self.counter: count = self.counter.resolve(context) context[self.countervar] = count - plural, vars = self.render_token_list(self.plural) + plural, plural_vars = self.render_token_list(self.plural) result = translation.ungettext(singular, plural, count) + vars.extend(plural_vars) else: result = translation.ugettext(singular) # Escape all isolated '%' before substituting in the context. result = re.sub(u'%(?!\()', u'%%', result) - data = dict([(v, _render_value_in_context(context[v], context)) for v in vars]) + data = dict([(v, _render_value_in_context(context.get(v, ''), context)) for v in vars]) context.pop() return result % data diff --git a/django/test/__init__.py b/django/test/__init__.py index 957b293e12d8..c996ed49d632 100644 --- a/django/test/__init__.py +++ b/django/test/__init__.py @@ -4,3 +4,4 @@ from django.test.client import Client from django.test.testcases import TestCase, TransactionTestCase +from django.test.utils import Approximate diff --git a/django/test/client.py b/django/test/client.py index e5a16b6e7989..a57793a2f6b9 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -3,6 +3,8 @@ import sys import os import re +import mimetypes +from copy import copy try: from cStringIO import StringIO except ImportError: @@ -54,6 +56,10 @@ class ClientHandler(BaseHandler): Uses the WSGI interface to compose requests, but returns the raw HttpResponse object """ + def __init__(self, enforce_csrf_checks=True, *args, **kwargs): + self.enforce_csrf_checks = enforce_csrf_checks + super(ClientHandler, self).__init__(*args, **kwargs) + def __call__(self, environ): from django.conf import settings from django.core import signals @@ -70,7 +76,7 @@ def __call__(self, environ): # CsrfViewMiddleware. This makes life easier, and is probably # required for backwards compatibility with external tests against # admin views. - request._dont_enforce_csrf_checks = True + request._dont_enforce_csrf_checks = not self.enforce_csrf_checks response = self.get_response(request) # Apply response middleware. @@ -87,9 +93,12 @@ def __call__(self, environ): def store_rendered_templates(store, signal, sender, template, context, **kwargs): """ Stores templates and contexts that are rendered. + + The context is copied so that it is an accurate representation at the time + of rendering. """ store.setdefault('template', []).append(template) - store.setdefault('context', ContextList()).append(context) + store.setdefault('context', ContextList()).append(copy(context)) def encode_multipart(boundary, data): """ @@ -138,11 +147,14 @@ def encode_multipart(boundary, data): def encode_file(boundary, key, file): to_str = lambda s: smart_str(s, settings.DEFAULT_CHARSET) + content_type = mimetypes.guess_type(file.name)[0] + if content_type is None: + content_type = 'application/octet-stream' return [ '--' + boundary, 'Content-Disposition: form-data; name="%s"; filename="%s"' \ % (to_str(key), to_str(os.path.basename(file.name))), - 'Content-Type: application/octet-stream', + 'Content-Type: %s' % content_type, '', file.read() ] @@ -165,8 +177,8 @@ class Client(object): contexts and templates produced by a view, rather than the HTML rendered to the end-user. """ - def __init__(self, **defaults): - self.handler = ClientHandler() + def __init__(self, enforce_csrf_checks=False, **defaults): + self.handler = ClientHandler(enforce_csrf_checks) self.defaults = defaults self.cookies = SimpleCookie() self.exc_info = None @@ -190,6 +202,13 @@ def _session(self): return {} session = property(_session) + def _get_path(self, parsed): + # If there are parameters, add them + if parsed[3]: + return urllib.unquote(parsed[2] + ";" + parsed[3]) + else: + return urllib.unquote(parsed[2]) + def request(self, **request): """ The master request method. Composes the environment dictionary @@ -280,7 +299,7 @@ def get(self, path, data={}, follow=False, **extra): parsed = urlparse(path) r = { 'CONTENT_TYPE': 'text/html; charset=utf-8', - 'PATH_INFO': urllib.unquote(parsed[2]), + 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], 'REQUEST_METHOD': 'GET', 'wsgi.input': FakePayload('') @@ -289,7 +308,7 @@ def get(self, path, data={}, follow=False, **extra): response = self.request(**r) if follow: - response = self._handle_redirects(response) + response = self._handle_redirects(response, **extra) return response def post(self, path, data={}, content_type=MULTIPART_CONTENT, @@ -312,7 +331,7 @@ def post(self, path, data={}, content_type=MULTIPART_CONTENT, r = { 'CONTENT_LENGTH': len(post_data), 'CONTENT_TYPE': content_type, - 'PATH_INFO': urllib.unquote(parsed[2]), + 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': parsed[4], 'REQUEST_METHOD': 'POST', 'wsgi.input': FakePayload(post_data), @@ -321,7 +340,7 @@ def post(self, path, data={}, content_type=MULTIPART_CONTENT, response = self.request(**r) if follow: - response = self._handle_redirects(response) + response = self._handle_redirects(response, **extra) return response def head(self, path, data={}, follow=False, **extra): @@ -331,7 +350,7 @@ def head(self, path, data={}, follow=False, **extra): parsed = urlparse(path) r = { 'CONTENT_TYPE': 'text/html; charset=utf-8', - 'PATH_INFO': urllib.unquote(parsed[2]), + 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], 'REQUEST_METHOD': 'HEAD', 'wsgi.input': FakePayload('') @@ -340,7 +359,7 @@ def head(self, path, data={}, follow=False, **extra): response = self.request(**r) if follow: - response = self._handle_redirects(response) + response = self._handle_redirects(response, **extra) return response def options(self, path, data={}, follow=False, **extra): @@ -349,7 +368,7 @@ def options(self, path, data={}, follow=False, **extra): """ parsed = urlparse(path) r = { - 'PATH_INFO': urllib.unquote(parsed[2]), + 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], 'REQUEST_METHOD': 'OPTIONS', 'wsgi.input': FakePayload('') @@ -358,7 +377,7 @@ def options(self, path, data={}, follow=False, **extra): response = self.request(**r) if follow: - response = self._handle_redirects(response) + response = self._handle_redirects(response, **extra) return response def put(self, path, data={}, content_type=MULTIPART_CONTENT, @@ -381,7 +400,7 @@ def put(self, path, data={}, content_type=MULTIPART_CONTENT, r = { 'CONTENT_LENGTH': len(post_data), 'CONTENT_TYPE': content_type, - 'PATH_INFO': urllib.unquote(parsed[2]), + 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': query_string or parsed[4], 'REQUEST_METHOD': 'PUT', 'wsgi.input': FakePayload(post_data), @@ -390,7 +409,7 @@ def put(self, path, data={}, content_type=MULTIPART_CONTENT, response = self.request(**r) if follow: - response = self._handle_redirects(response) + response = self._handle_redirects(response, **extra) return response def delete(self, path, data={}, follow=False, **extra): @@ -399,7 +418,7 @@ def delete(self, path, data={}, follow=False, **extra): """ parsed = urlparse(path) r = { - 'PATH_INFO': urllib.unquote(parsed[2]), + 'PATH_INFO': self._get_path(parsed), 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4], 'REQUEST_METHOD': 'DELETE', 'wsgi.input': FakePayload('') @@ -408,7 +427,7 @@ def delete(self, path, data={}, follow=False, **extra): response = self.request(**r) if follow: - response = self._handle_redirects(response) + response = self._handle_redirects(response, **extra) return response def login(self, **credentials): @@ -463,7 +482,7 @@ def logout(self): session.delete(session_key=session_cookie.value) self.cookies = SimpleCookie() - def _handle_redirects(self, response): + def _handle_redirects(self, response, **extra): "Follows any redirects by requesting responses from the server using GET." response.redirect_chain = [] @@ -474,7 +493,6 @@ def _handle_redirects(self, response): redirect_chain = response.redirect_chain redirect_chain.append((url, response.status_code)) - extra = {} if scheme: extra['wsgi.url_scheme'] = scheme diff --git a/django/test/simple.py b/django/test/simple.py index 9013042e6695..7d9332ef572e 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -3,11 +3,18 @@ import unittest from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.db.models import get_app, get_apps from django.test import _doctest as doctest from django.test.utils import setup_test_environment, teardown_test_environment from django.test.testcases import OutputChecker, DocTestRunner, TestCase + +try: + all +except NameError: + from django.utils.itercompat import all + # The module name for tests outside models.py TEST_MODULE = 'tests' @@ -222,6 +229,40 @@ def reorder_suite(suite, classes): bins[0].addTests(bins[i+1]) return bins[0] +def dependency_ordered(test_databases, dependencies): + """Reorder test_databases into an order that honors the dependencies + described in TEST_DEPENDENCIES. + """ + ordered_test_databases = [] + resolved_databases = set() + while test_databases: + changed = False + deferred = [] + + while test_databases: + signature, (db_name, aliases) = test_databases.pop() + dependencies_satisfied = True + for alias in aliases: + if alias in dependencies: + if all(a in resolved_databases for a in dependencies[alias]): + # all dependencies for this alias are satisfied + dependencies.pop(alias) + resolved_databases.add(alias) + else: + dependencies_satisfied = False + else: + resolved_databases.add(alias) + + if dependencies_satisfied: + ordered_test_databases.append((signature, (db_name, aliases))) + changed = True + else: + deferred.append((signature, (db_name, aliases))) + + if not changed: + raise ImproperlyConfigured("Circular dependency in TEST_DEPENDENCIES") + test_databases = deferred + return ordered_test_databases class DjangoTestSuiteRunner(object): def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs): @@ -254,20 +295,59 @@ def build_suite(self, test_labels, extra_tests=None, **kwargs): return reorder_suite(suite, (TestCase,)) def setup_databases(self, **kwargs): - from django.db import connections - old_names = [] - mirrors = [] + from django.db import connections, DEFAULT_DB_ALIAS + + # First pass -- work out which databases actually need to be created, + # and which ones are test mirrors or duplicate entries in DATABASES + mirrored_aliases = {} + test_databases = {} + dependencies = {} for alias in connections: connection = connections[alias] - # If the database is a test mirror, redirect it's connection - # instead of creating a test database. if connection.settings_dict['TEST_MIRROR']: - mirrors.append((alias, connection)) - mirror_alias = connection.settings_dict['TEST_MIRROR'] - connections._connections[alias] = connections[mirror_alias] + # If the database is marked as a test mirror, save + # the alias. + mirrored_aliases[alias] = connection.settings_dict['TEST_MIRROR'] else: - old_names.append((connection, connection.settings_dict['NAME'])) - connection.creation.create_test_db(self.verbosity, autoclobber=not self.interactive) + # Store a tuple with DB parameters that uniquely identify it. + # If we have two aliases with the same values for that tuple, + # we only need to create the test database once. + item = test_databases.setdefault( + connection.creation.test_db_signature(), + (connection.settings_dict['NAME'], []) + ) + item[1].append(alias) + + if 'TEST_DEPENDENCIES' in connection.settings_dict: + dependencies[alias] = connection.settings_dict['TEST_DEPENDENCIES'] + else: + if alias != DEFAULT_DB_ALIAS: + dependencies[alias] = connection.settings_dict.get('TEST_DEPENDENCIES', [DEFAULT_DB_ALIAS]) + + # Second pass -- actually create the databases. + old_names = [] + mirrors = [] + for signature, (db_name, aliases) in dependency_ordered(test_databases.items(), dependencies): + # Actually create the database for the first connection + connection = connections[aliases[0]] + old_names.append((connection, db_name, True)) + test_db_name = connection.creation.create_test_db(self.verbosity, autoclobber=not self.interactive) + for alias in aliases[1:]: + connection = connections[alias] + if db_name: + old_names.append((connection, db_name, False)) + connection.settings_dict['NAME'] = test_db_name + else: + # If settings_dict['NAME'] isn't defined, we have a backend where + # the name isn't important -- e.g., SQLite, which uses :memory:. + # Force create the database instead of assuming it's a duplicate. + old_names.append((connection, db_name, True)) + connection.creation.create_test_db(self.verbosity, autoclobber=not self.interactive) + + for alias, mirror_alias in mirrored_aliases.items(): + mirrors.append((alias, connections[alias].settings_dict['NAME'])) + connections[alias].settings_dict['NAME'] = connections[mirror_alias].settings_dict['NAME'] + return old_names, mirrors def run_suite(self, suite, **kwargs): @@ -277,11 +357,14 @@ def teardown_databases(self, old_config, **kwargs): from django.db import connections old_names, mirrors = old_config # Point all the mirrors back to the originals - for alias, connection in mirrors: - connections._connections[alias] = connection + for alias, old_name in mirrors: + connections[alias].settings_dict['NAME'] = old_name # Destroy all the non-mirror databases - for connection, old_name in old_names: - connection.creation.destroy_test_db(old_name, self.verbosity) + for connection, old_name, destroy in old_names: + if destroy: + connection.creation.destroy_test_db(old_name, self.verbosity) + else: + connection.settings_dict['NAME'] = old_name def teardown_test_environment(self, **kwargs): teardown_test_environment() diff --git a/django/test/testcases.py b/django/test/testcases.py index 276d1f3c4111..8dcbf019d190 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -11,6 +11,7 @@ from django.http import QueryDict from django.test import _doctest as doctest from django.test.client import Client +from django.test.utils import get_warnings_state, restore_warnings_state from django.utils import simplejson from django.utils.encoding import smart_str @@ -274,9 +275,20 @@ 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 connection in connections.all(): + connection.close() def _fixture_teardown(self): pass @@ -286,6 +298,19 @@ def _urlconf_teardown(self): settings.ROOT_URLCONF = self._old_root_urlconf clear_url_caches() + def save_warnings_state(self): + """ + Saves the state of the warnings module + """ + self._warnings_state = get_warnings_state() + + def restore_warnings_state(self): + """ + Restores the sate of the warnings module to the state + saved by save_warnings_state() + """ + restore_warnings_state(self._warnings_state) + 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 @@ -299,7 +324,7 @@ def assertRedirects(self, response, expected_url, status_code=302, if hasattr(response, 'redirect_chain'): # The request was a followed redirect - self.failUnless(len(response.redirect_chain) > 0, + self.assertTrue(len(response.redirect_chain) > 0, msg_prefix + "Response didn't redirect as expected: Response" " code was %d (expected %d)" % (response.status_code, status_code)) @@ -413,7 +438,7 @@ def assertFormError(self, response, form, field, errors, msg_prefix=''): if field: if field in context[form].errors: field_errors = context[form].errors[field] - self.failUnless(err in field_errors, + self.assertTrue(err in field_errors, msg_prefix + "The field '%s' on form '%s' in" " context %d does not contain the error '%s'" " (actual errors: %s)" % @@ -428,7 +453,7 @@ def assertFormError(self, response, form, field, errors, msg_prefix=''): (form, i, field)) else: non_field_errors = context[form].non_field_errors() - self.failUnless(err in non_field_errors, + self.assertTrue(err in non_field_errors, msg_prefix + "The form '%s' in context %d does not" " contain the non-field error '%s'" " (actual errors: %s)" % @@ -448,7 +473,7 @@ def assertTemplateUsed(self, response, template_name, msg_prefix=''): template_names = [t.name for t in to_list(response.template)] if not template_names: self.fail(msg_prefix + "No templates used to render the response") - self.failUnless(template_name in template_names, + self.assertTrue(template_name in template_names, msg_prefix + "Template '%s' was not a template used to render" " the response. Actual template(s) used: %s" % (template_name, u', '.join(template_names))) @@ -462,7 +487,7 @@ def assertTemplateNotUsed(self, response, template_name, msg_prefix=''): msg_prefix += ": " template_names = [t.name for t in to_list(response.template)] - self.failIf(template_name in template_names, + self.assertFalse(template_name in template_names, msg_prefix + "Template '%s' was used unexpectedly in rendering" " the response" % template_name) @@ -527,6 +552,3 @@ def _fixture_teardown(self): for db in databases: transaction.rollback(using=db) transaction.leave_transaction_management(using=db) - - for connection in connections.all(): - connection.close() diff --git a/django/test/utils.py b/django/test/utils.py index b6ab39901b4f..517d06ff0b3f 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -1,4 +1,7 @@ -import sys, time, os +import sys +import time +import os +import warnings from django.conf import settings from django.core import mail from django.core.mail.backends import locmem @@ -6,6 +9,21 @@ from django.template import Template from django.utils.translation import deactivate + +class Approximate(object): + def __init__(self, val, places=7): + self.val = val + self.places = places + + def __repr__(self): + return repr(self.val) + + def __eq__(self, other): + if self.val == other: + return True + return round(abs(self.val-other), self.places) == 0 + + class ContextList(list): """A wrapper that provides direct key access to context items contained in a list of context objects. @@ -19,6 +37,13 @@ def __getitem__(self, key): else: return super(ContextList, self).__getitem__(key) + def __contains__(self, key): + try: + value = self[key] + except KeyError: + return False + return True + def instrumented_test_render(self, context): """ @@ -49,6 +74,7 @@ def setup_test_environment(): deactivate() + def teardown_test_environment(): """Perform any global post-test teardown. This involves: @@ -67,6 +93,25 @@ def teardown_test_environment(): del mail.outbox + +def get_warnings_state(): + """ + Returns an object containing the state of the warnings module + """ + # There is no public interface for doing this, but this implementation of + # get_warnings_state and restore_warnings_state appears to work on Python + # 2.4 to 2.7. + return warnings.filters[:] + + +def restore_warnings_state(state): + """ + Restores the state of the warnings module when passed an object that was + returned by get_warnings_state() + """ + warnings.filters = state[:] + + def get_runner(settings): test_path = settings.TEST_RUNNER.split('.') # Allow for Python 2.5 relative paths diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 8d9d6f211918..ffa75e2c4f45 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -42,6 +42,10 @@ except ImportError: pass +try: + import termios +except ImportError: + termios = None RUN_RELOADER = True @@ -67,7 +71,17 @@ def code_changed(): return True return False +def ensure_echo_on(): + if termios: + fd = sys.stdin + if fd.isatty(): + attr_list = termios.tcgetattr(fd) + if not attr_list[3] & termios.ECHO: + attr_list[3] |= termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attr_list) + def reloader_thread(): + ensure_echo_on() while RUN_RELOADER: if code_changed(): sys.exit(3) # force reload @@ -75,7 +89,7 @@ def reloader_thread(): def restart_with_reloader(): while True: - args = [sys.executable] + sys.argv + args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] + sys.argv if sys.platform == "win32": args = ['"%s"' % arg for arg in args] new_environ = os.environ.copy() diff --git a/django/utils/cache.py b/django/utils/cache.py index 6cfd893668ae..58d087b0799c 100644 --- a/django/utils/cache.py +++ b/django/utils/cache.py @@ -134,6 +134,16 @@ def patch_vary_headers(response, newheaders): if newheader.lower() not in existing_headers] response['Vary'] = ', '.join(vary_headers + additional_headers) +def has_vary_header(response, header_query): + """ + Checks to see if the response has a given header name in its Vary header. + """ + if not response.has_header('Vary'): + return False + vary_headers = cc_delim_re.split(response['Vary']) + existing_headers = set([header.lower() for header in vary_headers]) + return header_query.lower() in existing_headers + def _i18n_cache_key_suffix(request, cache_key): """If enabled, returns the cache key ending with a locale.""" if settings.USE_I18N: diff --git a/django/utils/datastructures.py b/django/utils/datastructures.py index 3cbbe27b91c7..d73963fdce7a 100644 --- a/django/utils/datastructures.py +++ b/django/utils/datastructures.py @@ -99,9 +99,11 @@ def __init__(self, data=None): self.keyOrder = data.keys() else: self.keyOrder = [] + seen = set() for key, value in data: - if key not in self.keyOrder: + if key not in seen: self.keyOrder.append(key) + seen.add(key) def __deepcopy__(self, memo): return self.__class__([(key, deepcopy(value, memo)) diff --git a/django/utils/encoding.py b/django/utils/encoding.py index e2d72499031f..c843c197116c 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -151,6 +151,24 @@ def iri_to_uri(iri): return iri return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~") +def filepath_to_uri(path): + """Convert an file system path to a URI portion that is suitable for + inclusion in a URL. + + We are assuming input is either UTF-8 or unicode already. + + This method will encode certain chars that would normally be recognized as + special chars for URIs. Note that this method does not encode the ' + character, as it is a valid character within URIs. See + encodeURIComponent() JavaScript function for more details. + + Returns an ASCII string containing the encoded result. + """ + if path is None: + return path + # I know about `os.sep` and `os.altsep` but I want to leave + # some flexibility for hardcoding separators. + return urllib.quote(smart_str(path).replace("\\", "/"), safe="/~!*()'") # The encoding of the default system locale but falls back to the # given fallback encoding if the encoding is unsupported by python or could diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index 417531725002..b36b793cd266 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -7,7 +7,7 @@ >>> feed = feedgenerator.Rss201rev2Feed( ... title=u"Poynter E-Media Tidbits", ... link=u"http://www.poynter.org/column.asp?id=31", -... description=u"A group weblog by the sharpest minds in online media/journalism/publishing.", +... description=u"A group Weblog by the sharpest minds in online media/journalism/publishing.", ... language=u"en", ... ) >>> feed.add_item( @@ -27,8 +27,11 @@ import urlparse from django.utils.xmlutils import SimplerXMLGenerator from django.utils.encoding import force_unicode, iri_to_uri +from django.utils import datetime_safe def rfc2822_date(date): + # Support datetime objects older than 1900 + date = datetime_safe.new_datetime(date) # We do this ourselves to be timezone aware, email.Utils is not tz aware. if date.tzinfo: time_str = date.strftime('%a, %d %b %Y %H:%M:%S ') @@ -40,6 +43,8 @@ def rfc2822_date(date): return date.strftime('%a, %d %b %Y %H:%M:%S -0000') def rfc3339_date(date): + # Support datetime objects older than 1900 + date = datetime_safe.new_datetime(date) if date.tzinfo: time_str = date.strftime('%Y-%m-%dT%H:%M:%S') offset = date.tzinfo.utcoffset(date) @@ -64,7 +69,7 @@ def get_tag_uri(url, date): d = '' if date is not None: - d = ',%s' % date.strftime('%Y-%m-%d') + d = ',%s' % datetime_safe.new_datetime(date).strftime('%Y-%m-%d') return u'tag:%s%s:%s/%s' % (hostname, d, path, fragment) class SyndicationFeed(object): @@ -280,7 +285,7 @@ def add_item_elements(self, handler, item): class Atom1Feed(SyndicationFeed): # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html - mime_type = 'application/atom+xml' + mime_type = 'application/atom+xml; charset=utf8' ns = u"http://www.w3.org/2005/Atom" def write(self, outfile, encoding): diff --git a/django/utils/formats.py b/django/utils/formats.py index 31027abd23f5..48c27a6e17aa 100644 --- a/django/utils/formats.py +++ b/django/utils/formats.py @@ -6,32 +6,52 @@ from django.utils.importlib import import_module from django.utils.encoding import smart_str from django.utils import dateformat, numberformat, datetime_safe +from django.utils.safestring import mark_safe + +# format_cache is a mapping from (format_type, lang) to the format string. +# By using the cache, it is possible to avoid running get_format_modules +# repeatedly. +_format_cache = {} +_format_modules_cache = {} + +def reset_format_cache(): + """Clear any cached formats. + + This method is provided primarily for testing purposes, + so that the effects of cached formats can be removed. + """ + global _format_cache, _format_modules_cache + _format_cache = {} + _format_modules_cache = {} + +def iter_format_modules(lang): + """ + Does the heavy lifting of finding format modules. + """ + if check_for_language(lang): + format_locations = ['django.conf.locale.%s'] + if settings.FORMAT_MODULE_PATH: + format_locations.append(settings.FORMAT_MODULE_PATH + '.%s') + format_locations.reverse() + locale = to_locale(lang) + locales = [locale] + if '_' in locale: + locales.append(locale.split('_')[0]) + for location in format_locations: + for loc in locales: + try: + yield import_module('.formats', location % loc) + except ImportError: + pass def get_format_modules(reverse=False): """ - Returns an iterator over the format modules found in the project and Django + Returns a list of the format modules found """ - modules = [] - if not check_for_language(get_language()) or not settings.USE_L10N: - return modules - locale = to_locale(get_language()) - if settings.FORMAT_MODULE_PATH: - format_locations = [settings.FORMAT_MODULE_PATH + '.%s'] - else: - format_locations = [] - format_locations.append('django.conf.locale.%s') - for location in format_locations: - for l in (locale, locale.split('_')[0]): - try: - mod = import_module('.formats', location % l) - except ImportError: - pass - else: - # Don't return duplicates - if mod not in modules: - modules.append(mod) + lang = get_language() + modules = _format_modules_cache.setdefault(lang, list(iter_format_modules(lang))) if reverse: - modules.reverse() + return list(reversed(modules)) return modules def get_format(format_type): @@ -42,11 +62,18 @@ def get_format(format_type): """ format_type = smart_str(format_type) if settings.USE_L10N: - for module in get_format_modules(): - try: - return getattr(module, format_type) - except AttributeError: - pass + cache_key = (format_type, get_language()) + try: + return _format_cache[cache_key] or getattr(settings, format_type) + except KeyError: + for module in get_format_modules(): + try: + val = getattr(module, format_type) + _format_cache[cache_key] = val + return val + except AttributeError: + pass + _format_cache[cache_key] = None return getattr(settings, format_type) def date_format(value, format=None): @@ -79,23 +106,25 @@ def localize(value): Checks if value is a localizable type (date, number...) and returns it formatted as a string using current locale format """ - if settings.USE_L10N: - if isinstance(value, (decimal.Decimal, float, int)): - return number_format(value) - elif isinstance(value, datetime.datetime): - return date_format(value, 'DATETIME_FORMAT') - elif isinstance(value, datetime.date): - return date_format(value) - elif isinstance(value, datetime.time): - return time_format(value, 'TIME_FORMAT') - return value + if isinstance(value, bool): + return mark_safe(unicode(value)) + elif isinstance(value, (decimal.Decimal, float, int, long)): + return number_format(value) + elif isinstance(value, datetime.datetime): + return date_format(value, 'DATETIME_FORMAT') + elif isinstance(value, datetime.date): + return date_format(value) + elif isinstance(value, datetime.time): + return time_format(value, 'TIME_FORMAT') + else: + return value def localize_input(value, default=None): """ Checks if an input value is a localizable type and returns it formatted with the appropriate formatting string of the current locale. """ - if isinstance(value, (decimal.Decimal, float, int)): + if isinstance(value, (decimal.Decimal, float, int, long)): return number_format(value) if isinstance(value, datetime.datetime): value = datetime_safe.new_datetime(value) diff --git a/django/utils/html.py b/django/utils/html.py index 951b3f2a5925..094bc6660dae 100644 --- a/django/utils/html.py +++ b/django/utils/html.py @@ -34,6 +34,31 @@ def escape(html): return mark_safe(force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')) escape = allow_lazy(escape, unicode) +_base_js_escapes = ( + ('\\', r'\u005C'), + ('\'', r'\u0027'), + ('"', r'\u0022'), + ('>', r'\u003E'), + ('<', r'\u003C'), + ('&', r'\u0026'), + ('=', r'\u003D'), + ('-', r'\u002D'), + (';', r'\u003B'), + (u'\u2028', r'\u2028'), + (u'\u2029', r'\u2029') +) + +# Escape every ASCII character with a value less than 32. +_js_escapes = (_base_js_escapes + + tuple([('%c' % z, '\\u%04X' % z) for z in range(32)])) + +def escapejs(value): + """Hex encodes characters for use in JavaScript strings.""" + for bad, good in _js_escapes: + value = mark_safe(force_unicode(value).replace(bad, good)) + return value +escapejs = allow_lazy(escapejs, unicode) + def conditional_escape(html): """ Similar to escape(), except that it doesn't operate on pre-escaped strings. diff --git a/django/utils/http.py b/django/utils/http.py index f0b1af9c586d..50ad00e11b75 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -1,5 +1,9 @@ +import calendar +import datetime import re +import sys import urllib +import urlparse from email.Utils import formatdate from django.utils.encoding import smart_str, force_unicode @@ -7,6 +11,17 @@ ETAG_MATCH = re.compile(r'(?:W/)?"((?:\\.|[^"])*)"') +MONTHS = 'jan feb mar apr may jun jul aug sep oct nov dec'.split() +__D = r'(?P\d{2})' +__D2 = r'(?P[ \d]\d)' +__M = r'(?P\w{3})' +__Y = r'(?P\d{4})' +__Y2 = r'(?P\d{2})' +__T = r'(?P\d{2}):(?P\d{2}):(?P\d{2})' +RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T)) +RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T)) +ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y)) + def urlquote(url, safe='/'): """ A version of Python's urllib.quote() function that can operate on unicode @@ -69,13 +84,68 @@ def http_date(epoch_seconds=None): rfcdate = formatdate(epoch_seconds) return '%s GMT' % rfcdate[:25] +def parse_http_date(date): + """ + Parses a date format as specified by HTTP RFC2616 section 3.3.1. + + The three formats allowed by the RFC are accepted, even if only the first + one is still in widespread use. + + Returns an floating point number expressed in seconds since the epoch, in + UTC. + """ + # emails.Util.parsedate does the job for RFC1123 dates; unfortunately + # RFC2616 makes it mandatory to support RFC850 dates too. So we roll + # our own RFC-compliant parsing. + for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE: + m = regex.match(date) + if m is not None: + break + else: + raise ValueError("%r is not in a valid HTTP date format" % date) + try: + year = int(m.group('year')) + if year < 100: + if year < 70: + year += 2000 + else: + year += 1900 + month = MONTHS.index(m.group('mon').lower()) + 1 + day = int(m.group('day')) + hour = int(m.group('hour')) + min = int(m.group('min')) + sec = int(m.group('sec')) + result = datetime.datetime(year, month, day, hour, min, sec) + return calendar.timegm(result.utctimetuple()) + except Exception: + raise ValueError("%r is not a valid date" % date) + +def parse_http_date_safe(date): + """ + Same as parse_http_date, but returns None if the input is invalid. + """ + try: + return parse_http_date(date) + except Exception: + pass + # Base 36 functions: useful for generating compact URLs def base36_to_int(s): """ - Convertd a base 36 string to an integer + Converts a base 36 string to an ``int``. Raises ``ValueError` if the + input won't fit into an int. """ - return int(s, 36) + # To prevent overconsumption of server resources, reject any + # base36 string that is long than 13 base36 digits (13 digits + # is sufficient to base36-encode any 64-bit integer) + if len(s) > 13: + raise ValueError("Base36 input too large") + value = int(s, 36) + # ... then do a final check that the value will fit into an int. + if value > sys.maxint: + raise ValueError("Base36 input too large") + return value def int_to_base36(i): """ @@ -117,3 +187,20 @@ def quote_etag(etag): """ return '"%s"' % etag.replace('\\', '\\\\').replace('"', '\\"') +if sys.version_info >= (2, 6): + def same_origin(url1, url2): + """ + Checks if two URLs are 'same-origin' + """ + p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2) + return (p1.scheme, p1.hostname, p1.port) == (p2.scheme, p2.hostname, p2.port) +else: + # Python 2.4, 2.5 compatibility. This actually works for Python 2.6 and + # above, but the above definition is much more obviously correct and so is + # preferred going forward. + def same_origin(url1, url2): + """ + Checks if two URLs are 'same-origin' + """ + p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2) + return p1[0:2] == p2[0:2] diff --git a/django/utils/itercompat.py b/django/utils/itercompat.py index ab27c3ee018e..d4ff2503c7f4 100644 --- a/django/utils/itercompat.py +++ b/django/utils/itercompat.py @@ -37,3 +37,9 @@ def all(iterable): if not item: return False return True + +def any(iterable): + for item in iterable: + if item: + return True + return False diff --git a/django/utils/module_loading.py b/django/utils/module_loading.py index f2510353878f..32ca69a9fd77 100644 --- a/django/utils/module_loading.py +++ b/django/utils/module_loading.py @@ -6,8 +6,11 @@ def module_has_submodule(package, module_name): """See if 'module' is in 'package'.""" name = ".".join([package.__name__, module_name]) - if name in sys.modules: - return True + try: + # None indicates a cached miss; see mark_miss() in Python/import.c. + return sys.modules[name] is not None + except KeyError: + pass for finder in sys.meta_path: if finder.find_module(name): return True diff --git a/django/utils/numberformat.py b/django/utils/numberformat.py index 129c27f4da19..069f49851bdd 100644 --- a/django/utils/numberformat.py +++ b/django/utils/numberformat.py @@ -1,4 +1,6 @@ from django.conf import settings +from django.utils.safestring import mark_safe + def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''): """ @@ -11,15 +13,20 @@ def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''): * thousand_sep: Thousand separator symbol (for example ",") """ + use_grouping = settings.USE_L10N and \ + settings.USE_THOUSAND_SEPARATOR and grouping + # Make the common case fast: + if isinstance(number, int) and not use_grouping and not decimal_pos: + return mark_safe(unicode(number)) # sign if float(number) < 0: sign = '-' else: sign = '' - # decimal part str_number = unicode(number) if str_number[0] == '-': str_number = str_number[1:] + # decimal part if '.' in str_number: int_part, dec_part = str_number.split('.') if decimal_pos: @@ -30,13 +37,12 @@ def format(number, decimal_sep, decimal_pos, grouping=0, thousand_sep=''): dec_part = dec_part + ('0' * (decimal_pos - len(dec_part))) if dec_part: dec_part = decimal_sep + dec_part # grouping - if settings.USE_L10N and settings.USE_THOUSAND_SEPARATOR and grouping: + if use_grouping: int_part_gd = '' for cnt, digit in enumerate(int_part[::-1]): if cnt and not cnt % grouping: int_part_gd += thousand_sep int_part_gd += digit int_part = int_part_gd[::-1] - return sign + int_part + dec_part diff --git a/django/utils/text.py b/django/utils/text.py index 5d633b7afe06..b05460486dab 100644 --- a/django/utils/text.py +++ b/django/utils/text.py @@ -39,7 +39,10 @@ def _generator(): def truncate_words(s, num, end_text='...'): """Truncates a string after a certain number of words. Takes an optional argument of what should be used to notify that the string has been - truncated, defaults to ellipsis (...)""" + truncated, defaulting to ellipsis (...) + + Newlines in the string will be stripped. + """ s = force_unicode(s) length = int(num) words = s.split() @@ -51,10 +54,13 @@ def truncate_words(s, num, end_text='...'): truncate_words = allow_lazy(truncate_words, unicode) def truncate_html_words(s, num, end_text='...'): - """Truncates html to a certain number of words (not counting tags and + """Truncates HTML to a certain number of words (not counting tags and comments). Closes opened tags if they were correctly closed in the given html. Takes an optional argument of what should be used to notify that the - string has been truncated, defaults to ellipsis (...).""" + string has been truncated, defaulting to ellipsis (...). + + Newlines in the HTML are preserved. + """ s = force_unicode(s) length = int(num) if length <= 0: diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index b528f8e586dc..feb2f41ac149 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -439,10 +439,11 @@ def templatize(src): else: singular.append('%%(%s)s' % t.contents) elif t.token_type == TOKEN_TEXT: + contents = t.contents.replace('%', '%%') if inplural: - plural.append(t.contents) + plural.append(contents) else: - singular.append(t.contents) + singular.append(contents) else: if t.token_type == TOKEN_BLOCK: imatch = inline_re.match(t.contents) diff --git a/django/views/csrf.py b/django/views/csrf.py index fa996fff24c7..b590dd121346 100644 --- a/django/views/csrf.py +++ b/django/views/csrf.py @@ -23,7 +23,7 @@ h1 span { font-size:60%; color:#666; font-weight:normal; } #info { background:#f6f6f6; } #info ul { margin: 0.5em 4em; } - #info p { padding-top:10px; } + #info p, #summary p { padding-top:10px; } #summary { background: #ffc; } #explanation { background:#eee; border-bottom: 0px none; } @@ -32,6 +32,16 @@

          Forbidden (403)

          CSRF verification failed. Request aborted.

          +{% if no_referer %} +

          You are seeing this message because this HTTPS site requires a 'Referer + header' to be sent by your Web browser, but none was sent. This header is + required for security reasons, to ensure that your browser is not being + hijacked by third parties.

          + +

          If you have configured your browser to disable 'Referer' headers, please + re-enable them, at least for this site, or for HTTPS connections, or for + 'same-origin' requests.

          +{% endif %}
          {% if DEBUG %}
          @@ -83,7 +93,10 @@ def csrf_failure(request, reason=""): """ Default view used when request fails CSRF protection """ + from django.middleware.csrf import REASON_NO_REFERER t = Template(CSRF_FAILRE_TEMPLATE) c = Context({'DEBUG': settings.DEBUG, - 'reason': reason}) + 'reason': reason, + 'no_referer': reason == REASON_NO_REFERER + }) return HttpResponseForbidden(t.render(c), mimetype='text/html') diff --git a/django/views/debug.py b/django/views/debug.py index a396d3624485..d86c0ebf4b9f 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -2,6 +2,7 @@ import os import re import sys +import types from django.conf import settings from django.http import HttpResponse, HttpResponseServerError, HttpResponseNotFound @@ -87,7 +88,10 @@ def get_traceback_html(self): for loader in template_source_loaders: try: module = import_module(loader.__module__) - source_list_func = module.get_template_sources + if hasattr(loader, '__class__'): + source_list_func = loader.get_template_sources + else: # NOTE: Remember to remove this branch when we deprecate old template loaders in 1.4 + source_list_func = module.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)} \ @@ -96,7 +100,7 @@ def get_traceback_html(self): template_list = [] if hasattr(loader, '__class__'): loader_name = loader.__module__ + '.' + loader.__class__.__name__ - else: + else: # NOTE: Remember to remove this branch when we deprecate old template loaders in 1.4 loader_name = loader.__module__ + '.' + loader.__name__ self.loader_debug_info.append({ 'loader': loader_name, @@ -275,8 +279,13 @@ def technical_404_response(request, exception): # tried exists but is an empty list. The URLconf must've been empty. return empty_urlconf(request) + urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) + if isinstance(urlconf, types.ModuleType): + urlconf = urlconf.__name__ + t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 template') c = Context({ + 'urlconf': urlconf, 'root_urlconf': settings.ROOT_URLCONF, 'request_path': request.path_info[1:], # Trim leading slash 'urlpatterns': tried, @@ -773,7 +782,7 @@ def empty_urlconf(request):
          {% if urlpatterns %}

          - Using the URLconf defined in {{ settings.ROOT_URLCONF }}, + Using the URLconf defined in {{ urlconf }}, Django tried these URL patterns, in this order:

            diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index 2296d14daaa8..577c1ddab808 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -1,16 +1,3 @@ -""" -Decorator for views that tries getting the page from the cache and -populates the cache if the page isn't in the cache yet. - -The cache is keyed by the URL and some data from the headers. Additionally -there is the key prefix that is used to distinguish different cache areas -in a multi-site setup. You could use the sites.get_current().domain, for -example, as that is unique across a Django project. - -Additionally, all headers from the response's Vary header will be taken into -account on caching -- just like the middleware does. -""" - try: from functools import wraps except ImportError: @@ -22,6 +9,19 @@ def cache_page(*args, **kwargs): + """ + Decorator for views that tries getting the page from the cache and + populates the cache if the page isn't in the cache yet. + + The cache is keyed by the URL and some data from the headers. + Additionally there is the key prefix that is used to distinguish different + cache areas in a multi-site setup. You could use the + sites.get_current().domain, for example, as that is unique across a Django + project. + + Additionally, all headers from the response's Vary header will be taken + into account on caching -- just like the middleware does. + """ # We need backwards compatibility with code which spells it this way: # def my_view(): pass # my_view = cache_page(my_view, 123) @@ -33,6 +33,10 @@ def cache_page(*args, **kwargs): # my_view = cache_page(123, key_prefix="foo")(my_view) # and possibly this way (?): # my_view = cache_page(123, my_view) + # and also this way: + # my_view = cache_page(my_view) + # and also this way: + # my_view = cache_page()(my_view) # We also add some asserts to give better error messages in case people are # using other ways to call cache_page that no longer work. @@ -45,9 +49,14 @@ def cache_page(*args, **kwargs): elif callable(args[1]): return decorator_from_middleware_with_args(CacheMiddleware)(cache_timeout=args[0], key_prefix=key_prefix)(args[1]) else: - assert False, "cache_page must be passed either a single argument (timeout) or a view function and a timeout" + assert False, "cache_page must be passed a view function if called with two arguments" + elif len(args) == 1: + if callable(args[0]): + return decorator_from_middleware_with_args(CacheMiddleware)(key_prefix=key_prefix)(args[0]) + else: + return decorator_from_middleware_with_args(CacheMiddleware)(cache_timeout=args[0], key_prefix=key_prefix) else: - return decorator_from_middleware_with_args(CacheMiddleware)(cache_timeout=args[0], key_prefix=key_prefix) + return decorator_from_middleware_with_args(CacheMiddleware)(key_prefix=key_prefix) def cache_control(**kwargs): diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py index 782cb1ee89a9..89f676fe1669 100644 --- a/django/views/decorators/csrf.py +++ b/django/views/decorators/csrf.py @@ -14,6 +14,22 @@ 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. + def _reject(self, request, reason): + return None + + +requires_csrf_token = decorator_from_middleware(_EnsureCsrfToken) +requires_csrf_token.__name__ = 'requires_csrf_token' +csrf_protect.__doc__ = """ +Use this decorator on views that need a correct csrf_token available to +RequestContext, but without the CSRF protection that csrf_protect +enforces. +""" + def csrf_response_exempt(view_func): """ Modifies a view function so that its response is exempt diff --git a/django/views/decorators/gzip.py b/django/views/decorators/gzip.py index dc6edad04900..f5b009db4a60 100644 --- a/django/views/decorators/gzip.py +++ b/django/views/decorators/gzip.py @@ -1,6 +1,5 @@ -"Decorator for views that gzips pages if the client supports it." - from django.utils.decorators import decorator_from_middleware from django.middleware.gzip import GZipMiddleware gzip_page = decorator_from_middleware(GZipMiddleware) +gzip_page.__doc__ = "Decorator for views that gzips pages if the client supports it." diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index 220a60c4be1c..0032bf060074 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -9,10 +9,9 @@ from calendar import timegm from datetime import timedelta -from email.Utils import formatdate from django.utils.decorators import decorator_from_middleware, available_attrs -from django.utils.http import parse_etags, quote_etag +from django.utils.http import http_date, parse_http_date_safe, parse_etags, quote_etag from django.middleware.http import ConditionalGetMiddleware from django.http import HttpResponseNotAllowed, HttpResponseNotModified, HttpResponse @@ -70,6 +69,8 @@ def decorator(func): def inner(request, *args, **kwargs): # Get HTTP request headers if_modified_since = request.META.get("HTTP_IF_MODIFIED_SINCE") + if if_modified_since: + if_modified_since = parse_http_date_safe(if_modified_since) if_none_match = request.META.get("HTTP_IF_NONE_MATCH") if_match = request.META.get("HTTP_IF_MATCH") if if_none_match or if_match: @@ -93,7 +94,7 @@ def inner(request, *args, **kwargs): if last_modified_func: dt = last_modified_func(request, *args, **kwargs) if dt: - res_last_modified = formatdate(timegm(dt.utctimetuple()))[:26] + 'GMT' + res_last_modified = timegm(dt.utctimetuple()) else: res_last_modified = None else: @@ -107,7 +108,8 @@ def inner(request, *args, **kwargs): if ((if_none_match and (res_etag in etags or "*" in etags and res_etag)) and (not if_modified_since or - res_last_modified == if_modified_since)): + (res_last_modified and if_modified_since and + res_last_modified <= if_modified_since))): if request.method in ("GET", "HEAD"): response = HttpResponseNotModified() else: @@ -115,9 +117,9 @@ def inner(request, *args, **kwargs): elif if_match and ((not res_etag and "*" in etags) or (res_etag and res_etag not in etags)): response = HttpResponse(status=412) - elif (not if_none_match and if_modified_since and - request.method == "GET" and - res_last_modified == if_modified_since): + elif (not if_none_match and request.method == "GET" and + res_last_modified and if_modified_since and + res_last_modified <= if_modified_since): response = HttpResponseNotModified() if response is None: @@ -125,7 +127,7 @@ def inner(request, *args, **kwargs): # Set relevant headers on the response if they don't already exist. if res_last_modified and not response.has_header('Last-Modified'): - response['Last-Modified'] = res_last_modified + response['Last-Modified'] = http_date(res_last_modified) if res_etag and not response.has_header('ETag'): response['ETag'] = quote_etag(res_etag) diff --git a/django/views/defaults.py b/django/views/defaults.py index 68b9ad697c6d..29cdf8244d00 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,6 +1,11 @@ from django import http +from django.views.decorators.csrf import requires_csrf_token from django.template import Context, RequestContext, loader + +# This can be called when CsrfViewMiddleware.process_view has not run, therefore +# need @requires_csrf_token in case the template needs {% csrf_token %}. +@requires_csrf_token def page_not_found(request, template_name='404.html'): """ Default 404 handler. @@ -13,6 +18,8 @@ def page_not_found(request, template_name='404.html'): t = loader.get_template(template_name) # You need to create a 404.html template. return http.HttpResponseNotFound(t.render(RequestContext(request, {'request_path': request.path}))) + +@requires_csrf_token def server_error(request, template_name='500.html'): """ 500 error handler. @@ -23,6 +30,7 @@ def server_error(request, template_name='500.html'): t = loader.get_template(template_name) # You need to create a 500.html template. return http.HttpResponseServerError(t.render(Context({}))) + def shortcut(request, content_type_id, object_id): # TODO: Remove this in Django 2.0. # This is a legacy view that depends on the contenttypes framework. diff --git a/django/views/i18n.py b/django/views/i18n.py index 2078649e3d15..9043a8a0eeba 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -7,7 +7,7 @@ from django.utils.translation import check_for_language, activate, to_locale, get_language from django.utils.text import javascript_quote from django.utils.encoding import smart_unicode -from django.utils.formats import get_format_modules +from django.utils.formats import get_format_modules, get_format def set_language(request): """ @@ -49,10 +49,7 @@ def get_formats(): result = {} for module in [settings] + get_format_modules(reverse=True): for attr in FORMAT_SETTINGS: - try: - result[attr] = getattr(module, attr) - except AttributeError: - pass + result[attr] = get_format(attr) src = [] for k, v in result.items(): if isinstance(v, (basestring, int)): @@ -177,7 +174,8 @@ def javascript_catalog(request, domain='djangojs', packages=None): locale = to_locale(get_language()) t = {} paths = [] - en_catalog_missing = False + en_selected = locale.startswith('en') + en_catalog_missing = True # first load all english languages files for defaults for package in packages: p = importlib.import_module(package) @@ -187,13 +185,12 @@ def javascript_catalog(request, domain='djangojs', packages=None): catalog = gettext_module.translation(domain, path, ['en']) t.update(catalog._catalog) except IOError: - # 'en' catalog was missing. - if locale.startswith('en'): - # If 'en' is the selected language this would cause issues - # later on if default_locale is something other than 'en'. - en_catalog_missing = True - # Otherwise it is harmless. pass + else: + # 'en' is the selected language and at least one of the packages + # listed in `packages` has an 'en' catalog + if en_selected: + en_catalog_missing = False # next load the settings.LANGUAGE_CODE translations if it isn't english if default_locale != 'en': for path in paths: @@ -205,12 +202,11 @@ def javascript_catalog(request, domain='djangojs', packages=None): t.update(catalog._catalog) # last load the currently selected language, if it isn't identical to the default. if locale != default_locale: - # If the flag en_catalog_missing has been set, the currently - # selected language is English but it doesn't have a translation - # catalog (presumably due to being the language translated from). - # If that is the case, a wrong language catalog might have been - # loaded in the previous step. It needs to be discarded. - if en_catalog_missing: + # If the currently selected language is English but it doesn't have a + # translation catalog (presumably due to being the language translated + # from) then a wrong language catalog might have been loaded in the + # previous step. It needs to be discarded. + if en_selected and en_catalog_missing: t = {} else: locale_t = {} diff --git a/django/views/static.py b/django/views/static.py index d9117f282ed3..926bb473acce 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -9,12 +9,11 @@ import re import stat import urllib -from email.Utils import parsedate_tz, mktime_tz from django.template import loader from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseNotModified from django.template import Template, Context, TemplateDoesNotExist -from django.utils.http import http_date +from django.utils.http import http_date, parse_http_date def serve(request, path, document_root=None, show_indexes=False): """ @@ -56,7 +55,8 @@ def serve(request, path, document_root=None, show_indexes=False): raise Http404('"%s" does not exist' % fullpath) # Respect the If-Modified-Since header. statobj = os.stat(fullpath) - mimetype = mimetypes.guess_type(fullpath)[0] or 'application/octet-stream' + mimetype, encoding = mimetypes.guess_type(fullpath) + mimetype = mimetype or 'application/octet-stream' if not was_modified_since(request.META.get('HTTP_IF_MODIFIED_SINCE'), statobj[stat.ST_MTIME], statobj[stat.ST_SIZE]): return HttpResponseNotModified(mimetype=mimetype) @@ -64,6 +64,8 @@ def serve(request, path, document_root=None, show_indexes=False): response = HttpResponse(contents, mimetype=mimetype) response["Last-Modified"] = http_date(statobj[stat.ST_MTIME]) response["Content-Length"] = len(contents) + if encoding: + response["Content-Encoding"] = encoding return response DEFAULT_DIRECTORY_INDEX_TEMPLATE = """ @@ -126,12 +128,12 @@ def was_modified_since(header=None, mtime=0, size=0): raise ValueError matches = re.match(r"^([^;]+)(; length=([0-9]+))?$", header, re.IGNORECASE) - header_mtime = mktime_tz(parsedate_tz(matches.group(1))) + header_mtime = parse_http_date(matches.group(1)) header_len = matches.group(3) if header_len and int(header_len) != size: raise ValueError if mtime > header_mtime: raise ValueError - except (AttributeError, ValueError): + except (AttributeError, ValueError, OverflowError): return True return False diff --git a/docs/Makefile b/docs/Makefile index f6a92b6835a1..93013150405e 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -12,20 +12,26 @@ PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* @@ -40,6 +46,11 @@ dirhtml: @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @@ -65,12 +76,42 @@ qthelp: @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django.qhc" +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py index efcd94d4bc8c..8c4b51166c16 100644 --- a/docs/_ext/djangodocs.py +++ b/docs/_ext/djangodocs.py @@ -1,9 +1,10 @@ """ Sphinx plugins for Django documentation. """ +import os +import re -import docutils.nodes -import docutils.transforms +from docutils import nodes, transforms try: import json except ImportError: @@ -14,26 +15,16 @@ from django.utils import simplejson as json except ImportError: json = None -import os -import sphinx -import sphinx.addnodes -try: - from sphinx import builders -except ImportError: - import sphinx.builder as builders -try: - import sphinx.builders.html as builders_html -except ImportError: - builders_html = builders + +from sphinx import addnodes, roles +from sphinx.builders.html import StandaloneHTMLBuilder +from sphinx.writers.html import SmartyPantsHTMLTranslator from sphinx.util.console import bold -import sphinx.directives -import sphinx.environment -try: - import sphinx.writers.html as sphinx_htmlwriter -except ImportError: - import sphinx.htmlwriter as sphinx_htmlwriter -import sphinx.roles -from docutils import nodes +from sphinx.util.compat import Directive + +# RE for option descriptions without a '--' prefix +simple_option_desc_re = re.compile( + r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') def setup(app): app.add_crossref_type( @@ -69,63 +60,70 @@ def setup(app): parse_node = parse_django_adminopt_node, ) app.add_config_value('django_next_version', '0.0', True) - app.add_directive('versionadded', parse_version_directive, 1, (1, 1, 1)) - app.add_directive('versionchanged', parse_version_directive, 1, (1, 1, 1)) + app.add_directive('versionadded', VersionDirective) + app.add_directive('versionchanged', VersionDirective) app.add_transform(SuppressBlockquotes) app.add_builder(DjangoStandaloneHTMLBuilder) - # Monkeypatch PickleHTMLBuilder so that it doesn't die in Sphinx 0.4.2 - if sphinx.__version__ == '0.4.2': - monkeypatch_pickle_builder() -def parse_version_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - env = state.document.settings.env - is_nextversion = env.config.django_next_version == arguments[0] - ret = [] - node = sphinx.addnodes.versionmodified() - ret.append(node) - if not is_nextversion: - if len(arguments) == 1: - linktext = 'Please, see the release notes ' % (arguments[0]) - xrefs = sphinx.roles.xfileref_role('ref', linktext, linktext, lineno, state) - node.extend(xrefs[0]) - node['version'] = arguments[0] - else: - node['version'] = "Development version" - node['type'] = name - if len(arguments) == 2: - inodes, messages = state.inline_text(arguments[1], lineno+1) - node.extend(inodes) - if content: - state.nested_parse(content, content_offset, node) - ret = ret + messages - env.note_versionchange(node['type'], node['version'], node, lineno) - return ret +class VersionDirective(Directive): + has_content = True + required_arguments = 1 + optional_arguments = 1 + final_argument_whitespace = True + option_spec = {} + + def run(self): + env = self.state.document.settings.env + arg0 = self.arguments[0] + is_nextversion = env.config.django_next_version == arg0 + ret = [] + node = addnodes.versionmodified() + ret.append(node) + if not is_nextversion: + if len(self.arguments) == 1: + linktext = 'Please, see the release notes ' % (arg0) + try: + xrefs = roles.XRefRole()('doc', linktext, linktext, self.lineno, self.state) # Sphinx >= 1.0 + except AttributeError: + xrefs = roles.xfileref_role('doc', linktext, linktext, self.lineno, self.state) # Sphinx < 1.0 + node.extend(xrefs[0]) + node['version'] = arg0 + else: + node['version'] = "Development version" + node['type'] = self.name + if len(self.arguments) == 2: + inodes, messages = self.state.inline_text(self.arguments[1], self.lineno+1) + node.extend(inodes) + if self.content: + self.state.nested_parse(self.content, self.content_offset, node) + ret = ret + messages + env.note_versionchange(node['type'], node['version'], node, self.lineno) + return ret + - -class SuppressBlockquotes(docutils.transforms.Transform): +class SuppressBlockquotes(transforms.Transform): """ Remove the default blockquotes that encase indented list, tables, etc. """ default_priority = 300 - + suppress_blockquote_child_nodes = ( - docutils.nodes.bullet_list, - docutils.nodes.enumerated_list, - docutils.nodes.definition_list, - docutils.nodes.literal_block, - docutils.nodes.doctest_block, - docutils.nodes.line_block, - docutils.nodes.table + nodes.bullet_list, + nodes.enumerated_list, + nodes.definition_list, + nodes.literal_block, + nodes.doctest_block, + nodes.line_block, + nodes.table ) - + def apply(self): - for node in self.document.traverse(docutils.nodes.block_quote): + for node in self.document.traverse(nodes.block_quote): if len(node.children) == 1 and isinstance(node.children[0], self.suppress_blockquote_child_nodes): node.replace_self(node.children[0]) -class DjangoHTMLTranslator(sphinx_htmlwriter.SmartyPantsHTMLTranslator): +class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): """ Django-specific reST to HTML tweaks. """ @@ -133,42 +131,41 @@ class DjangoHTMLTranslator(sphinx_htmlwriter.SmartyPantsHTMLTranslator): # Don't use border=1, which docutils does by default. def visit_table(self, node): self.body.append(self.starttag(node, 'table', CLASS='docutils')) - + # ? Really? def visit_desc_parameterlist(self, node): self.body.append('(') self.first_param = 1 - + def depart_desc_parameterlist(self, node): self.body.append(')') - pass - + # # Don't apply smartypants to literal blocks # def visit_literal_block(self, node): self.no_smarty += 1 - sphinx_htmlwriter.SmartyPantsHTMLTranslator.visit_literal_block(self, node) + SmartyPantsHTMLTranslator.visit_literal_block(self, node) def depart_literal_block(self, node): - sphinx_htmlwriter.SmartyPantsHTMLTranslator.depart_literal_block(self, node) + SmartyPantsHTMLTranslator.depart_literal_block(self, node) self.no_smarty -= 1 - + # - # Turn the "new in version" stuff (versoinadded/versionchanged) into a + # Turn the "new in version" stuff (versionadded/versionchanged) into a # better callout -- the Sphinx default is just a little span, # which is a bit less obvious that I'd like. # - # FIXME: these messages are all hardcoded in English. We need to chanage + # FIXME: these messages are all hardcoded in English. We need to change # that to accomodate other language docs, but I can't work out how to make - # that work and I think it'll require Sphinx 0.5 anyway. + # that work. # version_text = { 'deprecated': 'Deprecated in Django %s', 'versionchanged': 'Changed in Django %s', 'versionadded': 'New in Django %s', } - + def visit_versionmodified(self, node): self.body.append( self.starttag(node, 'div', CLASS=node['type']) @@ -178,41 +175,31 @@ def visit_versionmodified(self, node): len(node) and ":" or "." ) self.body.append('%s ' % title) - + def depart_versionmodified(self, node): self.body.append("
          \n") - - # Give each section a unique ID -- nice for custom CSS hooks - # This is different on docutils 0.5 vs. 0.4... - if hasattr(sphinx_htmlwriter.SmartyPantsHTMLTranslator, 'start_tag_with_title') and sphinx.__version__ == '0.4.2': - def start_tag_with_title(self, node, tagname, **atts): - node = { - 'classes': node.get('classes', []), - 'ids': ['s-%s' % i for i in node.get('ids', [])] - } - return self.starttag(node, tagname, **atts) - - else: - def visit_section(self, node): - old_ids = node.get('ids', []) - node['ids'] = ['s-' + i for i in old_ids] - if sphinx.__version__ != '0.4.2': - node['ids'].extend(old_ids) - sphinx_htmlwriter.SmartyPantsHTMLTranslator.visit_section(self, node) - node['ids'] = old_ids + # Give each section a unique ID -- nice for custom CSS hooks + def visit_section(self, node): + old_ids = node.get('ids', []) + node['ids'] = ['s-' + i for i in old_ids] + node['ids'].extend(old_ids) + SmartyPantsHTMLTranslator.visit_section(self, node) + node['ids'] = old_ids def parse_django_admin_node(env, sig, signode): command = sig.split(' ')[0] env._django_curr_admin_command = command title = "django-admin.py %s" % sig - signode += sphinx.addnodes.desc_name(title, title) + signode += addnodes.desc_name(title, title) return sig def parse_django_adminopt_node(env, sig, signode): """A copy of sphinx.directives.CmdoptionDesc.parse_signature()""" - from sphinx import addnodes - from sphinx.directives.desc import option_desc_re + try: + from sphinx.domains.std import option_desc_re # Sphinx >= 1.0 + except ImportError: + from sphinx.directives.desc import option_desc_re # Sphinx < 1.0 count = 0 firstname = '' for m in option_desc_re.finditer(sig): @@ -224,48 +211,22 @@ def parse_django_adminopt_node(env, sig, signode): if not count: firstname = optname count += 1 + if not count: + for m in simple_option_desc_re.finditer(sig): + optname, args = m.groups() + if count: + signode += addnodes.desc_addname(', ', ', ') + signode += addnodes.desc_name(optname, optname) + signode += addnodes.desc_addname(args, args) + if not count: + firstname = optname + count += 1 if not firstname: raise ValueError return firstname -def monkeypatch_pickle_builder(): - import shutil - from os import path - try: - import cPickle as pickle - except ImportError: - import pickle - - def handle_finish(self): - # dump the global context - outfilename = path.join(self.outdir, 'globalcontext.pickle') - f = open(outfilename, 'wb') - try: - pickle.dump(self.globalcontext, f, 2) - finally: - f.close() - - self.info(bold('dumping search index...')) - self.indexer.prune(self.env.all_docs) - f = open(path.join(self.outdir, 'searchindex.pickle'), 'wb') - try: - self.indexer.dump(f, 'pickle') - finally: - f.close() - - # copy the environment file from the doctree dir to the output dir - # as needed by the web app - shutil.copyfile(path.join(self.doctreedir, builders.ENV_PICKLE_FILENAME), - path.join(self.outdir, builders.ENV_PICKLE_FILENAME)) - # touch 'last build' file, used by the web application to determine - # when to reload its environment and clear the cache - open(path.join(self.outdir, builders.LAST_BUILD_FILENAME), 'w').close() - - builders.PickleHTMLBuilder.handle_finish = handle_finish - - -class DjangoStandaloneHTMLBuilder(builders_html.StandaloneHTMLBuilder): +class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder): """ Subclass to add some extra things we need. """ @@ -278,9 +239,24 @@ def finish(self): self.warn("cannot create templatebuiltins.js due to missing simplejson dependency") return self.info(bold("writing templatebuiltins.js...")) - xrefs = self.env.reftargets.keys() - templatebuiltins = dict([('ttags', [n for (t,n) in xrefs if t == 'ttag']), - ('tfilters', [n for (t,n) in xrefs if t == 'tfilter'])]) + try: + # Sphinx < 1.0 + xrefs = self.env.reftargets.items() + templatebuiltins = dict([('ttags', [n for ((t,n),(l,a)) in xrefs + if t == 'ttag' and + l == 'ref/templates/builtins']), + ('tfilters', [n for ((t,n),(l,a)) in xrefs + if t == 'tfilter' and + l == 'ref/templates/builtins'])]) + except AttributeError: + # Sphinx >= 1.0 + xrefs = self.env.domaindata["std"]["objects"] + templatebuiltins = dict([('ttags', [n for ((t,n), (l,a)) in xrefs.items() + if t == 'templatetag' and + l == 'ref/templates/builtins' ]), + ('tfilters', [n for ((t,n), (l,a)) in xrefs.items() + if t == 'templatefilter' and + t == 'ref/templates/builtins'])]) outfilename = os.path.join(self.outdir, "templatebuiltins.js") f = open(outfilename, 'wb') f.write('var django_template_builtins = ') diff --git a/docs/_templates/genindex.html b/docs/_theme/djangodocs/genindex.html similarity index 68% rename from docs/_templates/genindex.html rename to docs/_theme/djangodocs/genindex.html index 60c19efd4547..486994ae911a 100644 --- a/docs/_templates/genindex.html +++ b/docs/_theme/djangodocs/genindex.html @@ -1,4 +1,4 @@ -{% extends "!genindex.html" %} +{% extends "basic/genindex.html" %} {% block bodyclass %}{% endblock %} {% block sidebarwrapper %}{% endblock %} \ No newline at end of file diff --git a/docs/_templates/layout.html b/docs/_theme/djangodocs/layout.html similarity index 97% rename from docs/_templates/layout.html rename to docs/_theme/djangodocs/layout.html index 70e029c3acf2..ef91dd77a92d 100644 --- a/docs/_templates/layout.html +++ b/docs/_theme/djangodocs/layout.html @@ -1,4 +1,4 @@ -{% extends "!layout.html" %} +{% extends "basic/layout.html" %} {%- macro secondnav() %} {%- if prev %} @@ -61,7 +61,7 @@

          {{ docstitle }}

          Home {{ reldelim2 }} Table of contents {{ reldelim2 }} Index {{ reldelim2 }} - Modules + Modules
          diff --git a/docs/_templates/modindex.html b/docs/_theme/djangodocs/modindex.html similarity index 67% rename from docs/_templates/modindex.html rename to docs/_theme/djangodocs/modindex.html index 96a1d2080aa7..59a5cb31bdf5 100644 --- a/docs/_templates/modindex.html +++ b/docs/_theme/djangodocs/modindex.html @@ -1,3 +1,3 @@ -{% extends "!modindex.html" %} +{% extends "basic/modindex.html" %} {% block bodyclass %}{% endblock %} {% block sidebarwrapper %}{% endblock %} \ No newline at end of file diff --git a/docs/_templates/search.html b/docs/_theme/djangodocs/search.html similarity index 69% rename from docs/_templates/search.html rename to docs/_theme/djangodocs/search.html index 8bd6dbd33220..943478ce7514 100644 --- a/docs/_templates/search.html +++ b/docs/_theme/djangodocs/search.html @@ -1,3 +1,3 @@ -{% extends "!search.html" %} +{% extends "basic/search.html" %} {% block bodyclass %}{% endblock %} {% block sidebarwrapper %}{% endblock %} \ No newline at end of file diff --git a/docs/_static/default.css b/docs/_theme/djangodocs/static/default.css similarity index 100% rename from docs/_static/default.css rename to docs/_theme/djangodocs/static/default.css diff --git a/docs/_static/djangodocs.css b/docs/_theme/djangodocs/static/djangodocs.css similarity index 100% rename from docs/_static/djangodocs.css rename to docs/_theme/djangodocs/static/djangodocs.css diff --git a/docs/_static/docicons-behindscenes.png b/docs/_theme/djangodocs/static/docicons-behindscenes.png similarity index 100% rename from docs/_static/docicons-behindscenes.png rename to docs/_theme/djangodocs/static/docicons-behindscenes.png diff --git a/docs/_static/docicons-note.png b/docs/_theme/djangodocs/static/docicons-note.png similarity index 100% rename from docs/_static/docicons-note.png rename to docs/_theme/djangodocs/static/docicons-note.png diff --git a/docs/_static/docicons-philosophy.png b/docs/_theme/djangodocs/static/docicons-philosophy.png similarity index 100% rename from docs/_static/docicons-philosophy.png rename to docs/_theme/djangodocs/static/docicons-philosophy.png diff --git a/docs/_static/homepage.css b/docs/_theme/djangodocs/static/homepage.css similarity index 100% rename from docs/_static/homepage.css rename to docs/_theme/djangodocs/static/homepage.css diff --git a/docs/_static/reset-fonts-grids.css b/docs/_theme/djangodocs/static/reset-fonts-grids.css similarity index 100% rename from docs/_static/reset-fonts-grids.css rename to docs/_theme/djangodocs/static/reset-fonts-grids.css diff --git a/docs/_theme/djangodocs/theme.conf b/docs/_theme/djangodocs/theme.conf new file mode 100644 index 000000000000..be43c723ae61 --- /dev/null +++ b/docs/_theme/djangodocs/theme.conf @@ -0,0 +1,4 @@ +[theme] +inherit = basic +stylesheet = default.css +pygments_style = trac diff --git a/docs/conf.py b/docs/conf.py index 90e0a6bcb5de..7b6e258a536d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -8,28 +8,35 @@ # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # -# All configuration values have a default value; values that are commented out -# serve to show the default value. +# All configuration values have a default; values that are commented out +# serve to show the default. import sys import os -# If your extensions are in another directory, add it here. -sys.path.append(os.path.join(os.path.dirname(__file__), "_ext")) +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext"))) -# General configuration -# --------------------- +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ["djangodocs"] # Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] +# templates_path = [] # The suffix of source filenames. source_suffix = '.txt' +# The encoding of source files. +#source_encoding = 'utf-8-sig' + # The master toctree document. master_doc = 'contents' @@ -37,24 +44,34 @@ project = 'Django' copyright = 'Django Software Foundation and contributors' -# The default replacements for |version| and |release|, also used in various -# other places throughout the built documents. + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. # # The short X.Y version. version = '1.2' # The full version, including alpha/beta/rc tags. -release = version +release = '1.2.7' # The next version to be released django_next_version = '1.3' +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' -# List of documents that shouldn't be included in the build. -#unused_docs = [] +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True @@ -70,18 +87,40 @@ # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'trac' -# Sphinx will recurse into subversion configuration folders and try to read -# any document file within. These should be ignored. -# Note: exclude_dirnames is new in Sphinx 0.5 +# Sphinx will recurse into subversion configuration folders and try to read +# any document file within. These should be ignored. +# Note: exclude_dirnames is new in Sphinx 0.5 exclude_dirnames = ['.svn'] -# Options for HTML output -# ----------------------- +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "djangodocs" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ["_theme"] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None -# The style sheet to use for HTML and HTML Help pages. A file of that name -# must exist either in Sphinx' static/ path, or in one of the custom paths -# given in html_static_path. -html_style = 'default.css' +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -110,17 +149,38 @@ html_additional_pages = {} # If false, no module index is generated. -#html_use_modindex = True +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True -# If true, the reST sources are included in the HTML build as _sources/. -html_copy_source = True +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'Djangodoc' +modindex_common_prefix = ["django."] + -# Options for LaTeX output -# ------------------------ +# -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' @@ -132,9 +192,24 @@ # (source start file, target name, title, author, document class [howto/manual]). #latex_documents = [] latex_documents = [ - ('contents', 'django.tex', 'Django Documentation', 'Django Software Foundation', 'manual'), + ('contents', 'django.tex', u'Django Documentation', + u'Django Software Foundation', 'manual'), ] +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + # Additional stuff for the LaTeX preamble. #latex_preamble = '' @@ -142,10 +217,53 @@ #latex_appendices = [] # If false, no module index is generated. -#latex_use_modindex = True +#latex_domain_indices = True -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# If this isn't set to True, the LaTex writer can only handle six levels of headers. -latex_use_parts = True +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('contents', 'django', 'Django Documentation', ['Django Software Foundation'], 1) +] + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'Django' +epub_author = u'Django Software Foundation' +epub_publisher = u'Django Software Foundation' +epub_copyright = u'2010, Django Software Foundation' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True diff --git a/docs/contents.txt b/docs/contents.txt index e41b77055e18..9bf0d685c4e9 100644 --- a/docs/contents.txt +++ b/docs/contents.txt @@ -5,17 +5,22 @@ Django documentation contents ============================= .. toctree:: - :maxdepth: 2 - - intro/index - topics/index - howto/index - faq/index - ref/index - misc/index - glossary - releases/index - internals/index + :hidden: + + index + +.. toctree:: + :maxdepth: 3 + + intro/index + topics/index + howto/index + faq/index + ref/index + misc/index + glossary + releases/index + internals/index Indices, glossary and tables ============================ @@ -32,5 +37,5 @@ have been replaced in newer versions of Django. .. toctree:: :maxdepth: 2 - - obsolete/index \ No newline at end of file + + obsolete/index diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index ed705d5f21bf..1512675c640a 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -1,5 +1,3 @@ -.. _faq-admin: - FAQ: The admin ============== @@ -32,29 +30,32 @@ How can I prevent the cache middleware from caching the admin site? ------------------------------------------------------------------- Set the :setting:`CACHE_MIDDLEWARE_ANONYMOUS_ONLY` setting to ``True``. See the -:ref:`cache documentation ` for more information. +:doc:`cache documentation ` for more information. How do I automatically set a field's value to the user who last edited the object in the admin? ----------------------------------------------------------------------------------------------- -The :class:`ModelAdmin` class provides customization hooks that allow you to transform -an object as it saved, using details from the request. By extracting the current user -from the request, and customizing the :meth:`ModelAdmin.save_model` hook, you can update -an object to reflect the user that edited it. See :ref:`the documentation on ModelAdmin -methods ` for an example. +The :class:`~django.contrib.admin.ModelAdmin` class provides customization hooks +that allow you to transform an object as it saved, using details from the +request. By extracting the current user from the request, and customizing the +:meth:`~django.contrib.admin.ModelAdmin.save_model` hook, you can update an +object to reflect the user that edited it. See :ref:`the documentation on +ModelAdmin methods ` for an example. How do I limit admin access so that objects can only be edited by the users who created them? --------------------------------------------------------------------------------------------- -The :class:`ModelAdmin` class also provides customization hooks that allow you to control the -visibility and editability of objects in the admin. Using the same trick of extracting the -user from the request, the :meth:`ModelAdmin.queryset` and :meth:`ModelAdmin.has_change_permission` -can be used to control the visibility and editability of objects in the admin. +The :class:`~django.contrib.admin.ModelAdmin` class also provides customization +hooks that allow you to control the visibility and editability of objects in the +admin. Using the same trick of extracting the user from the request, the +:meth:`~django.contrib.admin.ModelAdmin.queryset` and +:meth:`~django.contrib.admin.ModelAdmin.has_change_permission` can be used to +control the visibility and editability of objects in the admin. My admin-site CSS and images showed up fine using the development server, but they're not displaying when using mod_python. --------------------------------------------------------------------------------------------------------------------------- -See :ref:`serving the admin files ` +See :ref:`serving the admin files ` in the "How to use Django with mod_python" documentation. My "list_filter" contains a ManyToManyField, but the filter doesn't display. @@ -91,5 +92,5 @@ We like it, but if you don't agree, you can modify the admin site's presentation by editing the CSS stylesheet and/or associated image files. The site is built using semantic HTML and plenty of CSS hooks, so any changes you'd like to make should be possible by editing the stylesheet. We've got a -:ref:`guide to the CSS used in the admin ` to get you started. +:doc:`guide to the CSS used in the admin ` to get you started. diff --git a/docs/faq/contributing.txt b/docs/faq/contributing.txt index 51a9bc2c6a83..81c06f365fa0 100644 --- a/docs/faq/contributing.txt +++ b/docs/faq/contributing.txt @@ -1,5 +1,3 @@ -.. _faq-contributing: - FAQ: Contributing code ====================== @@ -7,7 +5,7 @@ How can I get started contributing code to Django? -------------------------------------------------- Thanks for asking! We've written an entire document devoted to this question. -It's titled :ref:`Contributing to Django `. +It's titled :doc:`Contributing to Django `. I submitted a bug fix in the ticket system several weeks ago. Why are you ignoring my patch? -------------------------------------------------------------------------------------------- diff --git a/docs/faq/general.txt b/docs/faq/general.txt index 1181d261be70..d83db681e62a 100644 --- a/docs/faq/general.txt +++ b/docs/faq/general.txt @@ -1,5 +1,3 @@ -.. _faq-general: - FAQ: General ============ @@ -63,25 +61,22 @@ at any level -- database servers, caching servers or Web/application servers. The framework cleanly separates components such as its database layer and application layer. And it ships with a simple-yet-powerful -:ref:`cache framework `. +:doc:`cache framework `. Who's behind this? ------------------ Django was originally developed at World Online, the Web department of a newspaper in Lawrence, Kansas, USA. Django's now run by an international team of -volunteers; you can read all about them over at the :ref:`list of committers -` +volunteers; you can read all about them over at the :doc:`list of committers +` Which sites use Django? ----------------------- -The Django wiki features a consistently growing `list of Django-powered sites`_. -Feel free to add your Django-powered site to the list. - -.. _list of Django-powered sites: http://code.djangoproject.com/wiki/DjangoPoweredSites +`DjangoSites.org`_ features a constantly growing list of Django-powered sites. -.. _mtv: +.. _DjangoSites.org: http://djangosites.org Django appears to be a MVC framework, but you call the Controller the "view", and the View the "template". How come you don't use the standard names? ----------------------------------------------------------------------------------------------------------------------------------------------------- @@ -146,7 +141,7 @@ philosophies 100%. Like we said: We're picky. We've documented our philosophies on the -:ref:`design philosophies page `. +:doc:`design philosophies page `. Is Django a content-management-system (CMS)? -------------------------------------------- @@ -169,14 +164,14 @@ How can I download the Django documentation to read it offline? --------------------------------------------------------------- The Django docs are available in the ``docs`` directory of each Django tarball -release. These docs are in ReST (ReStructured Text) format, and each text file +release. These docs are in reST (reStructuredText) format, and each text file corresponds to a Web page on the official Django site. Because the documentation is `stored in revision control`_, you can browse documentation changes just like you can browse code changes. Technically, the docs on Django's site are generated from the latest development -versions of those ReST documents, so the docs on the Django site may offer more +versions of those reST documents, so the docs on the Django site may offer more information than the docs that come with the latest Django release. .. _stored in revision control: http://code.djangoproject.com/browser/django/trunk/docs diff --git a/docs/faq/help.txt b/docs/faq/help.txt index 5d7faf6fec8b..d84b3f529fed 100644 --- a/docs/faq/help.txt +++ b/docs/faq/help.txt @@ -1,5 +1,3 @@ -.. _faq-help: - FAQ: Getting Help ================= diff --git a/docs/faq/index.txt b/docs/faq/index.txt index d357a3ebb0e3..347cabaabcea 100644 --- a/docs/faq/index.txt +++ b/docs/faq/index.txt @@ -1,5 +1,3 @@ -.. _faq-index: - ========== Django FAQ ========== diff --git a/docs/faq/install.txt b/docs/faq/install.txt index f20b2bc1872e..3fbcb3842d92 100644 --- a/docs/faq/install.txt +++ b/docs/faq/install.txt @@ -1,5 +1,3 @@ -.. _faq-install: - FAQ: Installation ================= @@ -7,9 +5,9 @@ How do I get started? --------------------- #. `Download the code`_. - #. Install Django (read the :ref:`installation guide `). - #. Walk through the :ref:`tutorial `. - #. Check out the rest of the :ref:`documentation `, and `ask questions`_ if you + #. Install Django (read the :doc:`installation guide `). + #. Walk through the :doc:`tutorial `. + #. Check out the rest of the :doc:`documentation `, and `ask questions`_ if you run into trouble. .. _`Download the code`: http://www.djangoproject.com/download/ @@ -19,14 +17,14 @@ What are Django's prerequisites? -------------------------------- Django requires Python_, specifically any version of Python from 2.4 -through 2.6. No other Python libraries are required for basic Django +through 2.7. No other Python libraries are required for basic Django usage. For a development environment -- if you just want to experiment with Django -- you don't need to have a separate Web server installed; Django comes with its own lightweight development server. For a production environment, Django follows the WSGI_ spec, which means it can run on a variety of server -platforms. See :ref:`Deploying Django ` for some +platforms. See :doc:`Deploying Django ` for some popular alternatives. Also, the `server arrangements wiki page`_ contains details for several deployment strategies. @@ -46,7 +44,7 @@ Do I lose anything by using Python 2.4 versus newer Python versions, such as Pyt ----------------------------------------------------------------------------------------------- Not in the core framework. Currently, Django itself officially supports any -version of Python from 2.4 through 2.6, inclusive. However, newer versions of +version of Python from 2.4 through 2.7, inclusive. However, newer versions of Python are often faster, have more features, and are better supported. Third-party applications for use with Django are, of course, free to set their own version requirements. @@ -56,7 +54,7 @@ versions as part of a migration which will end with Django running on Python 3 (see below for details). All else being equal, we recommend that you use the latest 2.x release -(currently Python 2.6). This will let you take advantage of the numerous +(currently Python 2.7). This will let you take advantage of the numerous improvements and optimizations to the Python language since version 2.4, and will help ease the process of dropping support for older Python versions on the road to Python 3. diff --git a/docs/faq/models.txt b/docs/faq/models.txt index 2732c0b8e19f..f00d453d887e 100644 --- a/docs/faq/models.txt +++ b/docs/faq/models.txt @@ -1,5 +1,3 @@ -.. _faq-models: - FAQ: Databases and models ========================= @@ -30,7 +28,7 @@ backend, and not all backends provide a way to retrieve the SQL after quoting. .. versionadded:: 1.2 -If you are using :ref:`multiple databases`, you can use the +If you are using :doc:`multiple databases`, you can use the same interface on each member of the ``connections`` dictionary:: >>> from django.db import connections @@ -39,7 +37,7 @@ same interface on each member of the ``connections`` dictionary:: Can I use Django with a pre-existing database? ---------------------------------------------- -Yes. See :ref:`Integrating with a legacy database `. +Yes. See :doc:`Integrating with a legacy database `. If I make changes to a model, how do I update the database? ----------------------------------------------------------- diff --git a/docs/faq/usage.txt b/docs/faq/usage.txt index 6c3c518bb277..c11514c4cdaf 100644 --- a/docs/faq/usage.txt +++ b/docs/faq/usage.txt @@ -1,5 +1,3 @@ -.. _faq-usage: - FAQ: Using Django ================= @@ -65,7 +63,7 @@ Using a :class:`~django.db.models.FileField` or an (relative to :setting:`MEDIA_ROOT`). You'll most likely want to use the convenience :attr:`~django.core.files.File.url` attribute provided by Django. For example, if your :class:`~django.db.models.ImageField` is - called ``mug_shot``, you can get the absolute URL to your image in a + called ``mug_shot``, you can get the absolute path to your image in a template with ``{{ object.mug_shot.url }}``. How do I make a variable available to all my templates? diff --git a/docs/glossary.txt b/docs/glossary.txt index 67a62ca31a1d..b8f7a6b9047a 100644 --- a/docs/glossary.txt +++ b/docs/glossary.txt @@ -9,19 +9,19 @@ Glossary field An attribute on a :term:`model`; a given field usually maps directly to a single database column. - - See :ref:`topics-db-models`. + + See :doc:`/topics/db/models`. generic view A higher-order :term:`view` function that provides an abstract/generic implementation of a common idiom or pattern found in view development. - - See :ref:`ref-generic-views`. + + See :doc:`/ref/generic-views`. model Models store your application's data. - - See :ref:`topics-db-models`. + + See :doc:`/topics/db/models`. MTV See :ref:`mtv`. @@ -41,7 +41,7 @@ Glossary property Also known as "managed attributes", and a feature of Python since version 2.2. From `the property documentation`__: - + Properties are a neat way to implement attributes whose usage resembles attribute access, but whose implementation uses method calls. [...] You @@ -56,26 +56,26 @@ Glossary queryset An object representing some set of rows to be fetched from the database. - - See :ref:`topics-db-queries`. + + See :doc:`/topics/db/queries`. slug A short label for something, containing only letters, numbers, underscores or hyphens. They're generally used in URLs. For example, in a typical blog entry URL: - + .. parsed-literal:: - + http://www.djangoproject.com/weblog/2008/apr/12/**spring**/ - + the last bit (``spring``) is the slug. template A chunk of text that acts as formatting for representing data. A template helps to abstract the presentation of data from the data itself. - - See :ref:`topics-templates`. - + + See :doc:`/topics/templates`. + view - A function responsible for rending a page. \ No newline at end of file + A function responsible for rending a page. diff --git a/docs/howto/apache-auth.txt b/docs/howto/apache-auth.txt index 8fd3da2612f9..2ebae0b736fd 100644 --- a/docs/howto/apache-auth.txt +++ b/docs/howto/apache-auth.txt @@ -1,12 +1,10 @@ -.. _howto-apache-auth: - ========================================================= Authenticating against Django's user database from Apache ========================================================= Since keeping multiple authentication databases in sync is a common problem when dealing with Apache, you can configuring Apache to authenticate against Django's -:ref:`authentication system ` directly. For example, you +:doc:`authentication system ` directly. For example, you could: * Serve static/media files directly from Apache only to authenticated users. diff --git a/docs/howto/auth-remote-user.txt b/docs/howto/auth-remote-user.txt index f0e83c0ba5e0..deab794cb11a 100644 --- a/docs/howto/auth-remote-user.txt +++ b/docs/howto/auth-remote-user.txt @@ -1,10 +1,8 @@ -.. _howto-auth-remote-user: - ==================================== Authentication using ``REMOTE_USER`` ==================================== -.. currentmodule:: django.contrib.backends +.. currentmodule:: django.contrib.auth.backends This document describes how to make use of external authentication sources (where the Web server sets the ``REMOTE_USER`` environment variable) in your @@ -70,7 +68,7 @@ If your authentication mechanism uses a custom HTTP header and not ``RemoteUserBackend`` ===================== -.. class:: django.contrib.backends.RemoteUserBackend +.. class:: django.contrib.auth.backends.RemoteUserBackend If you need more control, you can create your own authentication backend that inherits from ``RemoteUserBackend`` and overrides certain parts: diff --git a/docs/howto/contribute.txt b/docs/howto/contribute.txt new file mode 100644 index 000000000000..5d17ae69aa1f --- /dev/null +++ b/docs/howto/contribute.txt @@ -0,0 +1,321 @@ +=========================== +How to contribute to Django +=========================== + +Django is developed 100% by the community, and the more people that are actively +involved in the code the better Django will be. We recognize that contributing +to Django can be daunting at first and sometimes confusing even to +veterans. While we have our official "Contributing to Django" documentation +which spells out the technical details of triaging tickets and submitting +patches, it leaves a lot of room for interpretation. This guide aims to offer +more general advice on issues such as how to interpret the various stages and +flags in Trac, and how new contributors can get started. + +.. seealso:: + + This guide is meant to answer the most common questions about + contributing to Django, however it is no substitute for the + :doc:`/internals/contributing` reference. Please make sure to + read that document to understand the specific details + involved in reporting issues and submitting patches. + +.. _the-spirit-of-contributing: + +"The Spirit of Contributing" +============================ + +Django uses Trac_ for managing our progress, and Trac is a community-tended +garden of the bugs people have found and the features people would like to see +added. As in any garden, sometimes there are weeds to be pulled and sometimes +there are flowers and vegetables that need picking. We need your help to sort +out one from the other, and in the end we all benefit together. + +Like all gardens, we can aspire to perfection but in reality there's no such +thing. Even in the most pristine garden there are still snails and insects. In a +community garden there are also helpful people who--with the best of +intentions--fertilize the weeds and poison the roses. It's the job of the +community as a whole to self-manage, keep the problems to a minimum, and educate +those coming into the community so that they can become valuable contributing +members. + +Similarly, while we aim for Trac to be a perfect representation of the state of +Django's progress, we acknowledge that this simply will not happen. By +distributing the load of Trac maintenance to the community, we accept that there +will be mistakes. Trac is "mostly accurate", and we give allowances for the fact +that sometimes it will be wrong. That's okay. We're perfectionists with +deadlines. + +We rely on the community to keep participating, keep tickets as accurate as +possible, and raise issues for discussion on our mailing lists when there is +confusion or disagreement. + +Django is a community project, and every contribution helps. We can't do this +without YOU! + +.. _Trac: http://code.djangoproject.com/ + +Understanding Trac +================== + +Trac is Django's sole official issue tracker. All known bugs, desired features +and ideas for changes are logged there. + +However, Trac can be quite confusing even to veteran contributors. Having to +look at both flags and triage stages isn't immediately obvious, and the stages +themselves can be misinterpreted. + +.. _triage-stages-explained: + +What Django's triage stages "really mean" +----------------------------------------- + +Unreviewed +~~~~~~~~~~ + +The ticket has not been reviewed by anyone who felt qualified to make a judgment +about whether the ticket contained a valid issue, a viable feature, or ought to +be closed for any of the various reasons. + +Accepted +~~~~~~~~ + +The big grey area! The absolute meaning of "accepted" is that the issue +described in the ticket is valid and is in some stage of being worked on. Beyond +that there are several considerations + + +* **Accepted + No Flags** + + The ticket is valid, but no one has submitted a patch for it yet. Often this + means you could safely start writing a patch for it. + +* **Accepted + Has Patch** + + The ticket is waiting for people to review the supplied patch. This means + downloading the patch and trying it out, verifying that it contains tests and + docs, running the test suite with the included patch, and leaving feedback on + the ticket. + + +* **Accepted + Has Patch + (any other flag)** + + This means the ticket has been reviewed, and has been found to need further + work. "Needs tests" and "Needs documentation" are self-explanatory. "Patch + needs improvement" will generally be accompanied by a comment on the ticket + explaining what is needed to improve the code. + +Design Decision Needed +~~~~~~~~~~~~~~~~~~~~~~ + +This stage is for issues which may be contentious, may be backwards +incompatible, or otherwise involve high-level design decisions. These decisions +are generally made by the core committers, however that is not a +requirement. See the FAQ below for "My ticket has been in DDN forever! What +should I do?" + +Ready For Checkin +~~~~~~~~~~~~~~~~~ + +The ticket was reviewed by any member of the community other than the person who +supplied the patch and found to meet all the requirements for a commit-ready +patch. A core committer now needs to give the patch a final review prior to +being committed. See the FAQ below for "My ticket has been in RFC forever! What +should I do?" + +Someday/Maybe? +~~~~~~~~~~~~~~ + +Generally only used for vague/high-level features or design ideas. These tickets +are uncommon and overall less useful since they don't describe concrete +actionable issues. + +Fixed on a branch +~~~~~~~~~~~~~~~~~ + +Used to indicate that a ticket is resolved as part of a major body of work that +will eventually be merged to trunk. Tickets in this stage generally don't need +further work. This may happen in the case of major features/refactors in each +release cycle, or as part of the annual Google Summer of Code efforts. + +.. _closing-tickets: + +Closing Tickets +--------------- + +When a ticket has completed its useful lifecycle, it's time for it to be closed. +Closing a ticket is a big responsibility, though. You have to be sure that +the issue is really resolved, and you need to keep in mind that the reporter +of the ticket may not be happy to have their ticket closed (unless it's fixed, +of course). If you're not certain about closing a ticket, just leave a comment +with your thoughts instead. + +If you do close a ticket, you should always make sure of the following: + + * Be certain that the issue is resolved. + + * Leave a comment explaining the decision to close the ticket. + + * If there is a way they can improve the ticket to reopen it, let them know. + + * If the ticket is a duplicate, reference the original ticket. + + * **Be polite.** No one likes having their ticket closed. It can be + frustrating or even discouraging. The best way to avoid turning people + off from contributing to Django is to be polite and friendly and to offer + suggestions for how they could improve this ticket and other tickets in the + future. + +.. seealso:: + + The :ref:`contributing reference ` contains a + description of each of the available resolutions in Trac. + +Example Trac workflow +--------------------- + +Here we see the life-cycle of an average ticket: + +* Alice creates a ticket, and uploads an incomplete patch (no tests, incorrect + implementation). + +* Bob reviews the patch, marks it "Accepted", "needs tests", and "patch needs + improvement", and leaves a comment telling Alice how the patch could be + improved. + +* Alice updates the patch, adding tests (but not changing the + implementation). She removes the two flags. + +* Charlie reviews the patch and resets the "patch needs improvement" flag with + another comment about improving the implementation. + +* Alice updates the patch, fixing the implementation. She removes the "patch + needs improvement" flag. + +* Daisy reviews the patch, and marks it RFC. + +* Jacob reviews the RFC patch, applies it to his checkout, and commits it. + +Some tickets require much less feedback than this, but then again some tickets +require much much more. + +Advice for new contributors +=========================== + +New contributor and not sure what to do? Want to help but just don't know how to +get started? This is the section for you. + +* **Pick a subject area that you care about, that you are familiar with, or that + you want to learn about.** + + You don't already have to be an expert on the area you want to work on; you + become an expert through your ongoing contributions to the code. + +* **Triage tickets.** + + If a ticket is unreviewed and reports a bug, try and duplicate it. If you can + duplicate it and it seems valid, make a note that you confirmed the bug and + accept the ticket. Make sure the ticket is filed under the correct component + area. Consider writing a patch that adds a test for the bug's behavior, even + if you don't fix the bug itself. + +* **Look for tickets that are accepted and review patches to build familiarity + with the codebase and the process.** + + Mark the appropriate flags if a patch needs docs or tests. Look through the + changes a patch makes, and keep an eye out for syntax that is incompatible + with older but still supported versions of Python. Run the tests and make sure + they pass on your system. Where possible and relevant, try them out on a + database other than SQLite. Leave comments and feedback! + +* **Keep old patches up to date.** + + Oftentimes the codebase will change between a patch being submitted and the + time it gets reviewed. Make sure it still applies cleanly and functions as + expected. Simply updating a patch is both useful and important! + +* **Trac isn't an absolute; the context is just as important as the words.** + + When reading Trac, you need to take into account who says things, and when + they were said. Support for an idea two years ago doesn't necessarily mean + that the idea will still have support. You also need to pay attention to who + *hasn't* spoken -- for example, if a core team member hasn't been recently + involved in a discussion, then a ticket may not have the support required to + get into trunk. + +* **Start small.** + + It's easier to get feedback on a little issue than on a big one. + +* **If you're going to engage in a big task, make sure that your idea has + support first.** + + This means getting someone else to confirm that a bug is real before you fix + the issue, and ensuring that the core team supports a proposed feature before + you go implementing it. + +* **Be bold! Leave feedback!** + + Sometimes it can be scary to put your opinion out to the world and say "this + ticket is correct" or "this patch needs work", but it's the only way the + project moves forward. The contributions of the broad Django community + ultimately have a much greater impact than that of the core developers. We + can't do it without YOU! + +* **Err on the side of caution when marking things Ready For Check-in.** + + If you're really not certain if a ticket is ready, don't mark it as + such. Leave a comment instead, letting others know your thoughts. If you're + mostly certain, but not completely certain, you might also try asking on IRC + to see if someone else can confirm your suspicions. + +* **Wait for feedback, and respond to feedback that you receive.** + + Focus on one or two tickets, see them through from start to finish, and + repeat. The shotgun approach of taking on lots of tickets and letting some + fall by the wayside ends up doing more harm than good. + +* **Be rigorous.** + + When we say ":pep:`8`, and must have docs and tests", we mean it. If a patch + doesn't have docs and tests, there had better be a good reason. Arguments like + "I couldn't find any existing tests of this feature" don't carry much + weight--while it may be true, that means you have the extra-important job of + writing the very first tests for that feature, not that you get a pass from + writing tests altogether. + +.. note:: + + The `Reports page`_ contains links to many useful Trac queries, including + several that are useful for triaging tickets and reviewing patches as + suggested above. + + .. _Reports page: http://code.djangoproject.com/wiki/Reports + + +FAQs +==== + +**This ticket I care about has been ignored for days/weeks/months! What can I do +to get it committed?** + +* First off, it's not personal. Django is entirely developed by volunteers (even + the core devs), and sometimes folks just don't have time. The best thing to do + is to send a gentle reminder to the Django Developers mailing list asking for + review on the ticket, or to bring it up in the #django-dev IRC channel. + + +**I'm sure my ticket is absolutely 100% perfect, can I mark it as RFC myself?** + +* Short answer: No. It's always better to get another set of eyes on a + ticket. If you're having trouble getting that second set of eyes, see question + 1, above. + + +**My ticket has been in DDN forever! What should I do?** + +* Design Decision Needed requires consensus about the right solution. At the + very least it needs consensus among the core developers, and ideally it has + consensus from the community as well. The best way to accomplish this is to + start a thread on the Django Developers mailing list, and for very complex + issues to start a wiki page summarizing the problem and the possible + solutions. diff --git a/docs/howto/custom-file-storage.txt b/docs/howto/custom-file-storage.txt index 5005feaa809c..6a431f36a691 100644 --- a/docs/howto/custom-file-storage.txt +++ b/docs/howto/custom-file-storage.txt @@ -1,5 +1,3 @@ -.. _howto-custom-file-storage: - Writing a custom storage system =============================== @@ -37,7 +35,7 @@ You'll need to follow these steps: the ``path()`` method. Your custom storage system may override any of the storage methods explained in -:ref:`ref-files-storage`, but you **must** implement the following methods: +:doc:`/ref/files/storage`, but you **must** implement the following methods: * :meth:`Storage.delete` * :meth:`Storage.exists` @@ -63,14 +61,14 @@ backend storage system. Called by ``Storage.save()``. The ``name`` will already have gone through ``get_valid_name()`` and ``get_available_name()``, and the ``content`` will be a -``File`` object itself. +``File`` object itself. Should return the actual name of name of the file saved (usually the ``name`` passed in, but if the storage needs to change the file name return the new name instead). ``get_valid_name(name)`` ------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~ Returns a filename suitable for use with the underlying storage system. The ``name`` argument passed to this method is the original filename sent to the @@ -81,7 +79,7 @@ The code provided on ``Storage`` retains only alpha-numeric characters, periods and underscores from the original filename, removing everything else. ``get_available_name(name)`` ----------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Returns a filename that is available in the storage mechanism, possibly taking the provided filename into account. The ``name`` argument passed to this method diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 3f5feaa67ae3..2ab71e85acc2 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -1,16 +1,12 @@ -.. _howto-custom-management-commands: - ==================================== Writing custom django-admin commands ==================================== -.. versionadded:: 1.0 - Applications can register their own actions with ``manage.py``. For example, you might want to add a ``manage.py`` action for a Django app that you're distributing. In this document, we will be building a custom ``closepoll`` command for the ``polls`` application from the -:ref:`tutorial`. +:doc:`tutorial`. To do this, just add a ``management/commands`` directory to the application. Each Python module in that directory will be auto-discovered and registered as @@ -62,14 +58,7 @@ look like this: poll.opened = False poll.save() - self.stdout.write('Successfully closed poll "%s"\n' % poll_id) - -.. note:: - When you are using management commands and wish to provide console - output, you should write to ``self.stdout`` and ``self.stderr``, - instead of printing to ``stdout`` and ``stderr`` directly. By - using these proxies, it becomes much easier to test your custom - command. + print 'Successfully closed poll "%s"' % poll_id The new custom command can be called using ``python manage.py closepoll ``. @@ -77,7 +66,7 @@ The new custom command can be called using ``python manage.py closepoll The ``handle()`` method takes zero or more ``poll_ids`` and sets ``poll.opened`` to ``False`` for each one. If the user referenced any nonexistant polls, a :class:`CommandError` is raised. The ``poll.opened`` attribute does not exist -in the :ref:`tutorial` and was added to +in the :doc:`tutorial` and was added to ``polls.models.Poll`` for this example. The same ``closepoll`` could be easily modified to delete a given poll instead @@ -99,9 +88,58 @@ must be added to :attr:`~BaseCommand.option_list` like this: # ... In addition to being able to add custom command line options, all -:ref:`management commands` can accept some +:doc:`management commands` can accept some default options such as :djadminopt:`--verbosity` and :djadminopt:`--traceback`. +.. admonition:: Management commands and locales + + The :meth:`BaseCommand.execute` method sets the hardcoded ``en-us`` locale + because the commands shipped with Django perform several tasks + (for example, user-facing content rendering and database population) that + require a system-neutral string language (for which we use ``en-us``). + + If your custom management command uses another locale, you should manually + activate and deactivate it in your :meth:`~BaseCommand.handle` or + :meth:`~NoArgsCommand.handle_noargs` method using the functions provided by + the I18N support code: + + .. code-block:: python + + from django.core.management.base import BaseCommand, CommandError + from django.utils import translation + + class Command(BaseCommand): + ... + self.can_import_settings = True + + def handle(self, *args, **options): + + # Activate a fixed locale, e.g. Russian + translation.activate('ru') + + # Or you can activate the LANGUAGE_CODE + # chosen in the settings: + # + #from django.conf import settings + #translation.activate(settings.LANGUAGE_CODE) + + # Your command logic here + # ... + + translation.deactivate() + + Take into account though, that system management commands typically have to + be very careful about running in non-uniform locales, so: + + * Make sure the :setting:`USE_I18N` setting is always ``True`` when running + the command (this is one good example of the potential problems stemming + from a dynamic runtime environment that Django commands avoid offhand by + always using a fixed locale). + + * Review the code of your command and the code it calls for behavioral + differences when locales are changed and evaluate its impact on + predictable behavior of your command. + Command objects =============== diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 90851459c14b..5753abd56bf3 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -1,16 +1,13 @@ -.. _howto-custom-model-fields: - =========================== Writing custom model fields =========================== -.. versionadded:: 1.0 .. currentmodule:: django.db.models Introduction ============ -The :ref:`model reference ` documentation explains how to use +The :doc:`model reference ` documentation explains how to use Django's standard field classes -- :class:`~django.db.models.CharField`, :class:`~django.db.models.DateField`, etc. For many purposes, those classes are all you'll need. Sometimes, though, the Django version won't meet your precise @@ -108,8 +105,10 @@ say, all the *north* cards first, then the *east*, *south* and *west* cards. So What does a field class do? --------------------------- +.. class:: Field + All of Django's fields (and when we say *fields* in this document, we always -mean model fields and not :ref:`form fields `) are subclasses +mean model fields and not :doc:`form fields `) are subclasses of :class:`django.db.models.Field`. Most of the information that Django records about a field is common to all fields -- name, help text, uniqueness and so forth. Storing all that information is handled by ``Field``. We'll get into the @@ -124,7 +123,7 @@ when the model class is created (the precise details of how this is done are unimportant here). This is because the field classes aren't necessary when you're just creating and modifying attributes. Instead, they provide the machinery for converting between the attribute value and what is stored in the -database or sent to the :ref:`serializer `. +database or sent to the :doc:`serializer `. Keep this in mind when creating your own custom fields. The Django ``Field`` subclass you write provides the machinery for converting between your Python @@ -193,6 +192,8 @@ card values plus their suits; 104 characters in total. you want your fields to be more strict about the options they select, or to use the simpler, more permissive behavior of the current fields. +.. method:: Field.__init__ + The :meth:`~django.db.models.Field.__init__` method takes the following parameters: @@ -209,8 +210,8 @@ parameters: * :attr:`~django.db.models.Field.default` * :attr:`~django.db.models.Field.editable` * :attr:`~django.db.models.Field.serialize`: If ``False``, the field will - not be serialized when the model is passed to Django's :ref:`serializers - `. Defaults to ``True``. + not be serialized when the model is passed to Django's :doc:`serializers + `. Defaults to ``True``. * :attr:`~django.db.models.Field.unique_for_date` * :attr:`~django.db.models.Field.unique_for_month` * :attr:`~django.db.models.Field.unique_for_year` @@ -225,12 +226,14 @@ parameters: inheritance. For advanced use only. All of the options without an explanation in the above list have the same -meaning they do for normal Django fields. See the :ref:`field documentation -` for examples and details. +meaning they do for normal Django fields. See the :doc:`field documentation +` for examples and details. The ``SubfieldBase`` metaclass ------------------------------ +.. class:: django.db.models.SubfieldBase + As we indicated in the introduction_, field subclasses are often needed for two reasons: either to take advantage of a custom database column type, or to handle complex Python types. Obviously, a combination of the two is also @@ -245,8 +248,6 @@ appropriate Python object. The details of how this happens internally are a little complex, but the code you need to write in your ``Field`` class is simple: make sure your field subclass uses a special metaclass: -.. class:: django.db.models.SubfieldBase - For example:: class HandField(models.Field): @@ -258,20 +259,20 @@ For example:: def __init__(self, *args, **kwargs): # ... -This ensures that the :meth:`to_python` method, documented below, will always be -called when the attribute is initialized. +This ensures that the :meth:`.to_python` method, documented below, will always +be called when the attribute is initialized. ModelForms and custom fields ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you use :class:`~django.db.models.SubfieldBase`, :meth:`to_python` +If you use :class:`~django.db.models.SubfieldBase`, :meth:`.to_python` will be called every time an instance of the field is assigned a value. This means that whenever a value may be assigned to the field, you need to ensure that it will be of the correct datatype, or that you handle any exceptions. -This is especially important if you use :ref:`ModelForms -`. When saving a ModelForm, Django will use +This is especially important if you use :doc:`ModelForms +`. When saving a ModelForm, Django will use form values to instantiate model instances. However, if the cleaned form data can't be used as valid input to the field, the normal form validation process will break. @@ -280,24 +281,23 @@ Therefore, you must ensure that the form field used to represent your custom field performs whatever input validation and data cleaning is necessary to convert user-provided form input into a `to_python()`-compatible model field value. This may require writing a -custom form field, and/or implementing the :meth:`formfield` method on +custom form field, and/or implementing the :meth:`.formfield` method on your field to return a form field class whose `to_python()` returns the correct datatype. -Documenting your Custom Field +Documenting your custom field ----------------------------- -.. class:: django.db.models.Field - -.. attribute:: description +.. attribute:: Field.description As always, you should document your field type, so users will know what it is. In addition to providing a docstring for it, which is useful for developers, you can also allow users of the admin app to see a short description of the -field type via the ``django.contrib.admindocs`` application. To do this simply -provide descriptive text in a ``description`` class attribute of your custom field. -In the above example, the type description displayed by the ``admindocs`` application -for a ``HandField`` will be 'A hand of cards (bridge style)'. +field type via the :doc:`django.contrib.admindocs +` application. To do this simply provide +descriptive text in a ``description`` class attribute of your custom field. In +the above example, the description displayed by the ``admindocs`` +application for a ``HandField`` will be 'A hand of cards (bridge style)'. Useful methods -------------- @@ -310,7 +310,7 @@ approximately decreasing order of importance, so start from the top. Custom database types ~~~~~~~~~~~~~~~~~~~~~ -.. method:: db_type(self, connection) +.. method:: Field.db_type(self, connection) .. versionadded:: 1.2 The ``connection`` argument was added to support multiple databases. @@ -319,8 +319,8 @@ Returns the database column data type for the :class:`~django.db.models.Field`, taking into account the connection object, and the settings associated with it. Say you've created a PostgreSQL custom type called ``mytype``. You can use this -field with Django by subclassing ``Field`` and implementing the :meth:`db_type` -method, like so:: +field with Django by subclassing ``Field`` and implementing the +:meth:`.db_type` method, like so:: from django.db import models @@ -339,8 +339,8 @@ Once you have ``MytypeField``, you can use it in any model, just like any other If you aim to build a database-agnostic application, you should account for differences in database column types. For example, the date/time column type in PostgreSQL is called ``timestamp``, while the same column in MySQL is called -``datetime``. The simplest way to handle this in a ``db_type()`` method is to -check the ``connection.settings_dict['ENGINE']`` attribute. +``datetime``. The simplest way to handle this in a :meth:`.db_type` +method is to check the ``connection.settings_dict['ENGINE']`` attribute. For example:: @@ -351,11 +351,11 @@ For example:: else: return 'timestamp' -The :meth:`db_type` method is only called by Django when the framework -constructs the ``CREATE TABLE`` statements for your application -- that is, when -you first create your tables. It's not called at any other time, so it can -afford to execute slightly complex code, such as the ``connection.settings_dict`` -check in the above example. +The :meth:`.db_type` method is only called by Django when the framework +constructs the ``CREATE TABLE`` statements for your application -- that is, +when you first create your tables. It's not called at any other time, so it can +afford to execute slightly complex code, such as the +``connection.settings_dict`` check in the above example. Some database column types accept parameters, such as ``CHAR(25)``, where the parameter ``25`` represents the maximum column length. In cases like these, @@ -392,15 +392,15 @@ time -- i.e., when the class is instantiated. To do that, just implement my_field = BetterCharField(25) Finally, if your column requires truly complex SQL setup, return ``None`` from -:meth:`db_type`. This will cause Django's SQL creation code to skip over this -field. You are then responsible for creating the column in the right table in -some other way, of course, but this gives you a way to tell Django to get out of -the way. +:meth:`.db_type`. This will cause Django's SQL creation code to skip +over this field. You are then responsible for creating the column in the right +table in some other way, of course, but this gives you a way to tell Django to +get out of the way. Converting database values to Python objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: to_python(self, value) +.. method:: Field.to_python(self, value) Converts a value as returned by your database (or a serializer) to a Python object. @@ -422,7 +422,7 @@ with any of the following arguments: In our ``HandField`` class, we're storing the data as a VARCHAR field in the database, so we need to be able to process strings and ``Hand`` instances in -:meth:`to_python`:: +:meth:`.to_python`:: import re @@ -444,17 +444,18 @@ Python object type we want to store in the model's attribute. **Remember:** If your custom field needs the :meth:`to_python` method to be called when it is created, you should be using `The SubfieldBase metaclass`_ -mentioned earlier. Otherwise :meth:`to_python` won't be called automatically. +mentioned earlier. Otherwise :meth:`.to_python` won't be called +automatically. Converting Python objects to query values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: get_prep_value(self, value) +.. method:: Field.get_prep_value(self, value) .. versionadded:: 1.2 This method was factored out of ``get_db_prep_value()`` -This is the reverse of :meth:`to_python` when working with the +This is the reverse of :meth:`.to_python` when working with the database backends (as opposed to serialization). The ``value`` parameter is the current value of the model's attribute (a field has no reference to its containing model, so it cannot retrieve the value @@ -463,7 +464,7 @@ prepared for use as a parameter in a query. This conversion should *not* include any database-specific conversions. If database-specific conversions are required, they -should be made in the call to :meth:`get_db_prep_value`. +should be made in the call to :meth:`.get_db_prep_value`. For example:: @@ -477,43 +478,43 @@ For example:: Converting query values to database values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: get_db_prep_value(self, value, connection, prepared=False) +.. method:: Field.get_db_prep_value(self, value, connection, prepared=False) .. versionadded:: 1.2 The ``connection`` and ``prepared`` arguments were added to support multiple databases. Some data types (for example, dates) need to be in a specific format before they can be used by a database backend. -:meth:`get_db_prep_value` is the method where those conversions should +:meth:`.get_db_prep_value` is the method where those conversions should be made. The specific connection that will be used for the query is passed as the ``connection`` parameter. This allows you to use backend-specific conversion logic if it is required. The ``prepared`` argument describes whether or not the value has -already been passed through :meth:`get_prep_value` conversions. When +already been passed through :meth:`.get_prep_value` conversions. When ``prepared`` is False, the default implementation of -:meth:`get_db_prep_value` will call :meth:`get_prep_value` to do +:meth:`.get_db_prep_value` will call :meth:`.get_prep_value` to do initial data conversions before performing any database-specific processing. -.. method:: get_db_prep_save(self, value, connection) +.. method:: Field.get_db_prep_save(self, value, connection) .. versionadded:: 1.2 The ``connection`` argument was added to support multiple databases. Same as the above, but called when the Field value must be *saved* to the database. As the default implementation just calls -``get_db_prep_value``, you shouldn't need to implement this method +:meth:`.get_db_prep_value`, you shouldn't need to implement this method unless your custom field needs a special conversion when being saved that is not the same as the conversion used for normal query -parameters (which is implemented by ``get_db_prep_value``). +parameters (which is implemented by :meth:`.get_db_prep_value`). Preprocessing values before saving ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: pre_save(self, model_instance, add) +.. method:: Field.pre_save(self, model_instance, add) -This method is called just prior to :meth:`get_db_prep_save` and should return +This method is called just prior to :meth:`.get_db_prep_save` and should return the value of the appropriate attribute from ``model_instance`` for this field. The attribute name is in ``self.attname`` (this is set up by :class:`~django.db.models.Field`). If the model is being saved to the database @@ -537,12 +538,12 @@ Preparing values for use in database lookups As with value conversions, preparing a value for database lookups is a two phase process. -.. method:: get_prep_lookup(self, lookup_type, value) +.. method:: Field.get_prep_lookup(self, lookup_type, value) .. versionadded:: 1.2 This method was factored out of ``get_db_prep_lookup()`` -:meth:`get_prep_lookup` performs the first phase of lookup preparation, +:meth:`.get_prep_lookup` performs the first phase of lookup preparation, performing generic data validity checks Prepares the ``value`` for passing to the database when used in a lookup (a @@ -557,7 +558,7 @@ should raise either a ``ValueError`` if the ``value`` is of the wrong sort (a list when you were expecting an object, for example) or a ``TypeError`` if your field does not support that type of lookup. For many fields, you can get by with handling the lookup types that need special handling for your field -and pass the rest to the :meth:`get_db_prep_lookup` method of the parent class. +and pass the rest to the :meth:`.get_db_prep_lookup` method of the parent class. If you needed to implement ``get_db_prep_save()``, you will usually need to implement ``get_prep_lookup()``. If you don't, ``get_prep_value`` will be @@ -588,21 +589,21 @@ accepted lookup types to ``exact`` and ``in``:: else: raise TypeError('Lookup type %r not supported.' % lookup_type) -.. method:: get_db_prep_lookup(self, lookup_type, value, connection, prepared=False) +.. method:: Field.get_db_prep_lookup(self, lookup_type, value, connection, prepared=False) .. versionadded:: 1.2 The ``connection`` and ``prepared`` arguments were added to support multiple databases. Performs any database-specific data conversions required by a lookup. -As with :meth:`get_db_prep_value`, the specific connection that will +As with :meth:`.get_db_prep_value`, the specific connection that will be used for the query is passed as the ``connection`` parameter. The ``prepared`` argument describes whether the value has already been -prepared with :meth:`get_prep_lookup`. +prepared with :meth:`.get_prep_lookup`. Specifying the form field for a model field ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: formfield(self, form_class=forms.CharField, **kwargs) +.. method:: Field.formfield(self, form_class=forms.CharField, **kwargs) Returns the default form field to use when this field is displayed in a model. This method is called by the :class:`~django.forms.ModelForm` helper. @@ -611,11 +612,11 @@ All of the ``kwargs`` dictionary is passed directly to the form field's :meth:`~django.forms.Field__init__` method. Normally, all you need to do is set up a good default for the ``form_class`` argument and then delegate further handling to the parent class. This might require you to write a custom form -field (and even a form widget). See the :ref:`forms documentation -` for information about this, and take a look at the code in +field (and even a form widget). See the :doc:`forms documentation +` for information about this, and take a look at the code in :mod:`django.contrib.localflavor` for some examples of custom widgets. -Continuing our ongoing example, we can write the :meth:`formfield` method as:: +Continuing our ongoing example, we can write the :meth:`.formfield` method as:: class HandField(models.Field): # ... @@ -637,14 +638,14 @@ fields. Emulating built-in field types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: get_internal_type(self) +.. method:: Field.get_internal_type(self) Returns a string giving the name of the :class:`~django.db.models.Field` subclass we are emulating at the database level. This is used to determine the type of database column for simple cases. -If you have created a :meth:`db_type` method, you don't need to worry about -:meth:`get_internal_type` -- it won't be used much. Sometimes, though, your +If you have created a :meth:`.db_type` method, you don't need to worry about +:meth:`.get_internal_type` -- it won't be used much. Sometimes, though, your database storage is similar in type to some other field, so you can use that other field's logic to create the right column. @@ -659,11 +660,11 @@ For example:: No matter which database backend we are using, this will mean that ``syncdb`` and other SQL commands create the right column type for storing a string. -If :meth:`get_internal_type` returns a string that is not known to Django for +If :meth:`.get_internal_type` returns a string that is not known to Django for the database backend you are using -- that is, it doesn't appear in ``django.db.backends..creation.DATA_TYPES`` -- the string will still be -used by the serializer, but the default :meth:`db_type` method will return -``None``. See the documentation of :meth:`db_type` for reasons why this might be +used by the serializer, but the default :meth:`.db_type` method will return +``None``. See the documentation of :meth:`.db_type` for reasons why this might be useful. Putting a descriptive string in as the type of the field for the serializer is a useful idea if you're ever going to be using the serializer output in some other place, outside of Django. @@ -671,7 +672,7 @@ output in some other place, outside of Django. Converting field data for serialization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. method:: value_to_string(self, obj) +.. method:: Field.value_to_string(self, obj) This method is used by the serializers to convert the field into a string for output. Calling :meth:`Field._get_val_from_obj(obj)` is the best way to get the @@ -721,7 +722,7 @@ Django provides a ``File`` class, which is used as a proxy to the file's contents and operations. This can be subclassed to customize how the file is accessed, and what methods are available. It lives at ``django.db.models.fields.files``, and its default behavior is explained in the -:ref:`file documentation `. +:doc:`file documentation `. Once a subclass of ``File`` is created, the new ``FileField`` subclass must be told to use it. To do so, simply assign the new ``File`` subclass to the special diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 1406d19b588f..b4d364ab0aa4 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -1,5 +1,3 @@ -.. _howto-custom-template-tags: - ================================ Custom template tags and filters ================================ @@ -7,8 +5,8 @@ Custom template tags and filters Introduction ============ -Django's template system comes with a wide variety of :ref:`built-in -tags and filters ` designed to address the +Django's template system comes with a wide variety of :doc:`built-in +tags and filters ` designed to address the presentation logic needs of your application. Nevertheless, you may find yourself needing functionality that is not covered by the core set of template primitives. You can extend the template engine by @@ -157,8 +155,6 @@ will use the function's name as the filter name. Filters and auto-escaping ~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.0 - When writing a custom filter, give some thought to how the filter will interact with Django's auto-escaping behavior. Note that three types of strings can be passed around inside the template code: @@ -428,8 +424,6 @@ without having to be parsed multiple times. Auto-escaping considerations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. versionadded:: 1.0 - The output from template tags is **not** automatically run through the auto-escaping filters. However, there are still a couple of things you should keep in mind when writing a template tag. @@ -607,10 +601,6 @@ Now your tag should begin to look like this:: raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name return FormatTimeNode(date_to_be_formatted, format_string[1:-1]) -.. versionchanged:: 1.0 - Variable resolution has changed in the 1.0 release of Django. ``template.resolve_variable()`` - has been deprecated in favor of a new ``template.Variable`` class. - You also have to change the renderer to retrieve the actual contents of the ``date_updated`` property of the ``blog_entry`` object. This can be accomplished by using the ``Variable()`` class in ``django.template``. @@ -792,7 +782,7 @@ difference between this case and the previous ``inclusion_tag`` example. Setting a variable in the context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The above example simply output a value. Generally, it's more flexible if your +The above examples simply output a value. Generally, it's more flexible if your template tags set template variables instead of outputting values. That way, template authors can reuse the values that your template tags create. @@ -818,6 +808,13 @@ Here's how you'd use this new version of the tag: {% current_time "%Y-%M-%d %I:%M %p" %}

          The time is {{ current_time }}.

          +.. admonition:: Variable scope in context + + Any variable set in the context will only be available in the same ``block`` + of the template in which it was assigned. This behaviour is intentional; + it provides a scope for variables so that they don't conflict with + context in other blocks. + But, there's a problem with ``CurrentTimeNode2``: The variable name ``current_time`` is hard-coded. This means you'll need to make sure your template doesn't use ``{{ current_time }}`` anywhere else, because the diff --git a/docs/howto/deployment/fastcgi.txt b/docs/howto/deployment/fastcgi.txt index cf05174390d4..ea14b97ff117 100644 --- a/docs/howto/deployment/fastcgi.txt +++ b/docs/howto/deployment/fastcgi.txt @@ -1,13 +1,11 @@ -.. _howto-deployment-fastcgi: - ============================================ How to use Django with FastCGI, SCGI, or AJP ============================================ .. highlight:: bash -Although the current preferred setup for running Django is :ref:`Apache with -mod_wsgi `, many people use shared hosting, on +Although the current preferred setup for running Django is :doc:`Apache with +mod_wsgi `, many people use shared hosting, on which protocols such as FastCGI, SCGI or AJP are the only viable options. In some setups, these protocols may provide better performance than mod_wsgi_. @@ -74,7 +72,7 @@ TCP socket. What you choose is a manner of preference; a TCP socket is usually easier due to permissions issues. To start your server, first change into the directory of your project (wherever -your :ref:`manage.py ` is), and then run the +your :doc:`manage.py ` is), and then run the :djadmin:`runfcgi` command:: ./manage.py runfcgi [options] @@ -82,19 +80,19 @@ your :ref:`manage.py ` is), and then run the If you specify ``help`` as the only option after :djadmin:`runfcgi`, it'll display a list of all the available options. -You'll need to specify either a ``socket``, a ``protocol`` or both ``host`` and -``port``. Then, when you set up your Web server, you'll just need to point it at -the host/port or socket you specified when starting the FastCGI server. See the -examples_, below. +You'll need to specify either a :djadminopt:`socket`, a :djadminopt:`protocol` +or both :djadminopt:`host` and :djadminopt:`port`. Then, when you set up your +Web server, you'll just need to point it at the host/port or socket you +specified when starting the FastCGI server. See the examples_, below. Protocols --------- Django supports all the protocols that flup_ does, namely fastcgi_, `SCGI`_ and `AJP1.3`_ (the Apache JServ Protocol, version 1.3). Select your preferred -protocol by using the ``protocol=`` option with ``./manage.py -runfcgi`` -- where ```` may be one of: ``fcgi`` (the default), -``scgi`` or ``ajp``. For example:: +protocol by using the :djadminopt:`protocol=\ ` option +with ``./manage.py runfcgi`` -- where ```` may be one of: +``fcgi`` (the default), ``scgi`` or ``ajp``. For example:: ./manage.py runfcgi protocol=scgi @@ -113,6 +111,14 @@ Running a threaded server on a TCP port:: Running a preforked server on a Unix domain socket:: ./manage.py runfcgi method=prefork socket=/home/user/mysite.sock pidfile=django.pid + +.. admonition:: Socket security + + Django's default umask requires that the webserver and the Django fastcgi + process be run with the same group **and** user. For increased security, + you can run them under the same group but as different users. If you do + this, you will need to set the umask to 0002 using the ``umask`` argument + to ``runfcgi``. Run without daemonizing (backgrounding) the process (good for debugging):: @@ -126,8 +132,8 @@ Simply hitting ``Ctrl-C`` will stop and quit the FastCGI server. However, when you're dealing with background processes, you'll need to resort to the Unix ``kill`` command. -If you specify the ``pidfile`` option to :djadmin:`runfcgi`, you can kill the -running FastCGI daemon like this:: +If you specify the :djadminopt:`pidfile` option to :djadmin:`runfcgi`, you can +kill the running FastCGI daemon like this:: kill `cat $PIDFILE` @@ -370,21 +376,21 @@ Forcing the URL prefix to a particular value ============================================ Because many of these fastcgi-based solutions require rewriting the URL at -some point inside the webserver, the path information that Django sees may not +some point inside the Web server, the path information that Django sees may not resemble the original URL that was passed in. This is a problem if the Django application is being served from under a particular prefix and you want your URLs from the ``{% url %}`` tag to look like the prefix, rather than the rewritten version, which might contain, for example, ``mysite.fcgi``. Django makes a good attempt to work out what the real script name prefix -should be. In particular, if the webserver sets the ``SCRIPT_URL`` (specific +should be. In particular, if the Web server sets the ``SCRIPT_URL`` (specific to Apache's mod_rewrite), or ``REDIRECT_URL`` (set by a few servers, including Apache + mod_rewrite in some situations), Django will work out the original prefix automatically. In the cases where Django cannot work out the prefix correctly and where you want the original value to be used in URLs, you can set the -``FORCE_SCRIPT_NAME`` setting in your main ``settings`` file. This sets the +:setting:`FORCE_SCRIPT_NAME` setting in your main ``settings`` file. This sets the script name uniformly for every URL served via that settings file. Thus you'll need to use different settings files if you want different sets of URLs to have different script names in this case, but that is a rare situation. diff --git a/docs/howto/deployment/index.txt b/docs/howto/deployment/index.txt index 78cfb037f538..740f9bc8d66f 100644 --- a/docs/howto/deployment/index.txt +++ b/docs/howto/deployment/index.txt @@ -1,27 +1,25 @@ -.. _howto-deployment-index: - Deploying Django ================ -Django's chock-full of shortcuts to make web developer's lives easier, but all +Django's chock-full of shortcuts to make Web developer's lives easier, but all those tools are of no use if you can't easily deploy your sites. Since Django's inception, ease of deployment has been a major goal. There's a number of good ways to easily deploy Django: .. toctree:: :maxdepth: 1 - + modwsgi modpython fastcgi - + If you're new to deploying Django and/or Python, we'd recommend you try -:ref:`mod_wsgi ` first. In most cases it'll be the easiest, +:doc:`mod_wsgi ` first. In most cases it'll be the easiest, fastest, and most stable deployment choice. .. seealso:: * `Chapter 12 of The Django Book`_ discusses deployment and especially scaling in more detail. - + .. _chapter 12 of the django book: http://djangobook.com/en/2.0/chapter12/ diff --git a/docs/howto/deployment/modpython.txt b/docs/howto/deployment/modpython.txt index 143a6d5ae319..8355a511987e 100644 --- a/docs/howto/deployment/modpython.txt +++ b/docs/howto/deployment/modpython.txt @@ -4,11 +4,18 @@ How to use Django with Apache and mod_python ============================================ +.. warning:: + + Support for mod_python will be deprecated in a future release of Django. If + you are configuring a new deployment, you are strongly encouraged to + consider using :doc:`mod_wsgi ` or any of the + other :doc:`supported backends `. + .. highlight:: apache The `mod_python`_ module for Apache_ can be used to deploy Django to a production server, although it has been mostly superseded by the simpler -:ref:`mod_wsgi deployment option `. +:doc:`mod_wsgi deployment option `. mod_python is similar to (and inspired by) `mod_perl`_ : It embeds Python within Apache and loads Python code into memory when the server starts. Code stays in @@ -25,8 +32,8 @@ Django requires Apache 2.x and mod_python 3.x, and you should use Apache's Apache, there's no better source than `Apache's own official documentation`_ - * You may also be interested in :ref:`How to use Django with FastCGI, SCGI, - or AJP `. + * You may also be interested in :doc:`How to use Django with FastCGI, SCGI, + or AJP `. .. _Apache: http://httpd.apache.org/ .. _mod_python: http://www.modpython.org/ @@ -58,9 +65,6 @@ This tells Apache: "Use mod_python for any URL at or under '/mysite/', using the Django mod_python handler." It passes the value of :ref:`DJANGO_SETTINGS_MODULE ` so mod_python knows which settings to use. -.. versionadded:: 1.0 - The ``PythonOption django.root ...`` is new in this version. - Because mod_python does not know we are serving this site from underneath the ``/mysite/`` prefix, this value needs to be passed through to the mod_python handler in Django, via the ``PythonOption django.root ...`` line. The value set @@ -153,6 +157,8 @@ the full URL. When deploying Django sites on mod_python, you'll need to restart Apache each time you make changes to your Python code. +.. _mod_python documentation: http://modpython.org/live/current/doc-html/directives.html + Multiple Django installations on the same Apache ================================================ @@ -206,15 +212,25 @@ everything for each request. But don't do that on a production server, or we'll revoke your Django privileges. If you're the type of programmer who debugs using scattered ``print`` -statements, note that ``print`` statements have no effect in mod_python; they -don't appear in the Apache log, as one might expect. If you have the need to -print debugging information in a mod_python setup, either do this:: +statements, note that output to ``stdout`` will not appear in the Apache +log and can even `cause response errors`_. - assert False, the_value_i_want_to_see +.. _cause response errors: http://blog.dscpl.com.au/2009/04/wsgi-and-printing-to-standard-output.html -Or add the debugging information to the template of your page. +If you have the need to print debugging information in a mod_python setup, you +have a few options. You can print to ``stderr`` explicitly, like so:: -.. _mod_python documentation: http://modpython.org/live/current/doc-html/directives.html + print >> sys.stderr, 'debug text' + sys.stderr.flush() + +(note that ``stderr`` is buffered, so calling ``flush`` is necessary if you wish +debugging information to be displayed promptly.) + +A more compact approach is to use an assertion:: + + assert False, 'debug text' + +Another alternative is to add debugging information to the template of your page. .. _serving-media-files: @@ -269,8 +285,6 @@ the ``media`` subdirectory and any URL that ends with ``.jpg``, ``.gif`` or .. _Apache: http://httpd.apache.org/ .. _Cherokee: http://www.cherokee-project.com/ -.. _howto-deployment-modpython-serving-the-admin-files: - .. _serving-the-admin-files: Serving the admin files @@ -306,7 +320,7 @@ project (or somewhere else) that contains something like the following: import os os.environ['PYTHON_EGG_CACHE'] = '/some/directory' -Here, ``/some/directory`` is a directory that the Apache webserver process can +Here, ``/some/directory`` is a directory that the Apache Web server process can write to. It will be used as the location for any unpacking of code the eggs need to do. @@ -383,7 +397,7 @@ If you get a UnicodeEncodeError =============================== If you're taking advantage of the internationalization features of Django (see -:ref:`topics-i18n`) and you intend to allow users to upload files, you must +:doc:`/topics/i18n/index`) and you intend to allow users to upload files, you must ensure that the environment used to start Apache is configured to accept non-ASCII file names. If your environment is not correctly configured, you will trigger ``UnicodeEncodeError`` exceptions when calling functions like diff --git a/docs/howto/deployment/modwsgi.txt b/docs/howto/deployment/modwsgi.txt index 12de53f53d5f..24d169b942df 100644 --- a/docs/howto/deployment/modwsgi.txt +++ b/docs/howto/deployment/modwsgi.txt @@ -1,5 +1,3 @@ -.. _howto-deployment-modwsgi: - ========================================== How to use Django with Apache and mod_wsgi ========================================== @@ -23,7 +21,7 @@ the details about how to use mod_wsgi. You'll probably want to start with the .. _official mod_wsgi documentation: http://code.google.com/p/modwsgi/ .. _installation and configuration documentation: http://code.google.com/p/modwsgi/wiki/InstallationInstructions -Basic Configuration +Basic configuration =================== Once you've got mod_wsgi installed and activated, edit your ``httpd.conf`` file @@ -49,10 +47,12 @@ mentioned in the second part of ``WSGIScriptAlias`` and add:: If your project is not on your ``PYTHONPATH`` by default you can add:: - sys.path.append('/usr/local/django') + path = '/path/to/mysite' + if path not in sys.path: + sys.path.append(path) -just above the final ``import`` line to place your project on the path. Remember to -replace 'mysite.settings' with your correct settings file, and '/usr/local/django' +just below the ``import sys`` line to place your project on the path. Remember to +replace 'mysite.settings' with your correct settings file, and '/path/to/mysite' with your own project's location. Serving media files @@ -81,7 +81,7 @@ file. All other URLs will be served using mod_wsgi:: Alias /robots.txt /usr/local/wsgi/static/robots.txt Alias /favicon.ico /usr/local/wsgi/static/favicon.ico - AliasMatch /([^/]*\.css) /usr/local/wsgi/static/styles/$1 + AliasMatch ^/([^/]*\.css) /usr/local/wsgi/static/styles/$1 Alias /media/ /usr/local/wsgi/static/media/ diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 97842d7263dd..9c61c97f658e 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -1,5 +1,3 @@ -.. _howto-error-reporting: - Error reporting via e-mail ========================== @@ -11,7 +9,7 @@ revealed by the error pages. However, running with :setting:`DEBUG` set to ``False`` means you'll never see errors generated by your site -- everyone will just see your public error pages. You need to keep track of errors that occur in deployed sites, so Django can be -configured to email you details of those errors. +configured to e-mail you details of those errors. Server errors ------------- @@ -30,12 +28,12 @@ the HTTP request that caused the error. to specify :setting:`EMAIL_HOST` and possibly :setting:`EMAIL_HOST_USER` and :setting:`EMAIL_HOST_PASSWORD`, though other settings may be also required depending on your mail - server's configuration. Consult :ref:`the Django settings - documentation ` for a full list of email-related + server's configuration. Consult :doc:`the Django settings + documentation ` for a full list of email-related settings. -By default, Django will send email from root@localhost. However, some mail -providers reject all email from this address. To use a different sender +By default, Django will send e-mail from root@localhost. However, some mail +providers reject all e-mail from this address. To use a different sender address, modify the :setting:`SERVER_EMAIL` setting. To disable this behavior, just remove all entries from the :setting:`ADMINS` @@ -44,8 +42,8 @@ setting. 404 errors ---------- -Django can also be configured to email errors about broken links (404 "page -not found" errors). Django sends emails about 404 errors when: +Django can also be configured to e-mail errors about broken links (404 "page +not found" errors). Django sends e-mails about 404 errors when: * :setting:`DEBUG` is ``False`` @@ -57,7 +55,7 @@ not found" errors). Django sends emails about 404 errors when: If those conditions are met, Django will e-mail the users listed in the :setting:`MANAGERS` setting whenever your code raises a 404 and the request has a referer. (It doesn't bother to e-mail for 404s that don't have a referer -- -those are usually just people typing in broken URLs or broken web 'bots). +those are usually just people typing in broken URLs or broken Web 'bots). You can tell Django to stop reporting particular 404s by tweaking the :setting:`IGNORABLE_404_ENDS` and :setting:`IGNORABLE_404_STARTS` settings. Both diff --git a/docs/howto/i18n.txt b/docs/howto/i18n.txt index 853162aa70b8..64b33d765f85 100644 --- a/docs/howto/i18n.txt +++ b/docs/howto/i18n.txt @@ -1,5 +1,3 @@ -.. _howto-i18n: - .. _using-translations-in-your-own-projects: =============================================== @@ -8,11 +6,11 @@ Using internationalization in your own projects At runtime, Django looks for translations by following this algorithm: - * First, it looks for a ``locale`` directory in the application directory - of the view that's being called. If it finds a translation for the - selected language, the translation will be installed. - * Next, it looks for a ``locale`` directory in the project directory. If it - finds a translation, the translation will be installed. + * First, it looks for a ``locale`` directory in the directory containing + your settings file. + * Second, it looks for a ``locale`` directory in the project directory. + * Third, it looks for a ``locale`` directory in each of the installed apps. + It does this in the reverse order of INSTALLED_APPS * Finally, it checks the Django-provided base translation in ``django/conf/locale``. @@ -46,7 +44,7 @@ To create message files, you use the :djadmin:`django-admin.py makemessages ` to produce the binary ``.mo`` files that are used by ``gettext``. Read the -:ref:`topics-i18n-localization` document for more details. +:doc:`/topics/i18n/localization` document for more details. You can also run ``django-admin.py compilemessages --settings=path.to.settings`` to make the compiler process all the directories in your :setting:`LOCALE_PATHS` diff --git a/docs/howto/index.txt b/docs/howto/index.txt index c582c8ed171b..7ce7d26ceb83 100644 --- a/docs/howto/index.txt +++ b/docs/howto/index.txt @@ -1,11 +1,9 @@ -.. _howto-index: - "How-to" guides =============== Here you'll find short answers to "How do I....?" types of questions. These how-to guides don't cover topics in depth -- you'll find that material in the -:ref:`topics-index` and the :ref:`ref-index`. However, these guides will help +:doc:`/topics/index` and the :doc:`/ref/index`. However, these guides will help you quickly accomplish common tasks. .. toctree:: @@ -13,6 +11,7 @@ you quickly accomplish common tasks. apache-auth auth-remote-user + contribute custom-management-commands custom-model-fields custom-template-tags diff --git a/docs/howto/initial-data.txt b/docs/howto/initial-data.txt index b071d6d5295e..dd6a099b9d7f 100644 --- a/docs/howto/initial-data.txt +++ b/docs/howto/initial-data.txt @@ -1,5 +1,3 @@ -.. _howto-initial-data: - ================================= Providing initial data for models ================================= @@ -20,10 +18,10 @@ Providing initial data with fixtures A fixture is a collection of data that Django knows how to import into a database. The most straightforward way of creating a fixture if you've already -got some data is to use the :djadmin:`manage.py dumpdata` command. Or, you can -write fixtures by hand; fixtures can be written as XML, YAML, or JSON documents. -The :ref:`serialization documentation ` has more details -about each of these supported :ref:`serialization formats +got some data is to use the :djadmin:`manage.py dumpdata ` command. +Or, you can write fixtures by hand; fixtures can be written as XML, YAML, or +JSON documents. The :doc:`serialization documentation ` +has more details about each of these supported :ref:`serialization formats `. As an example, though, here's what a fixture for a simple ``Person`` model might @@ -114,15 +112,26 @@ which will insert the desired data (e.g., properly-formatted ``INSERT`` statements separated by semicolons). The SQL files are read by the :djadmin:`sqlcustom`, :djadmin:`sqlreset`, -:djadmin:`sqlall` and :djadmin:`reset` commands in :ref:`manage.py -`. Refer to the :ref:`manage.py documentation -` for more information. +:djadmin:`sqlall` and :djadmin:`reset` commands in :doc:`manage.py +`. Refer to the :doc:`manage.py documentation +` for more information. Note that if you have multiple SQL data files, there's no guarantee of the order in which they're executed. The only thing you can assume is that, by the time your custom data files are executed, all the database tables already will have been created. +.. admonition:: Initial SQL data and testing + + This technique *cannot* be used to provide initial data for + testing purposes. Django's test framework flushes the contents of + the test database after each test; as a result, any data added + using the custom SQL hook will be lost. + + If you require data for a test case, you should add it using + either a :ref:`test fixture `, or + programatically add it during the ``setUp()`` of your test case. + Database-backend-specific SQL data ---------------------------------- diff --git a/docs/howto/jython.txt b/docs/howto/jython.txt index 385790e9e6e1..1bf8d6c1f455 100644 --- a/docs/howto/jython.txt +++ b/docs/howto/jython.txt @@ -1,5 +1,3 @@ -.. _howto-jython: - ======================== Running Django on Jython ======================== @@ -53,7 +51,7 @@ on top of Jython. .. _`django-jython`: http://code.google.com/p/django-jython/ To install it, follow the `installation instructions`_ detailed on the project -website. Also, read the `database backends`_ documentation there. +Web site. Also, read the `database backends`_ documentation there. .. _`installation instructions`: http://code.google.com/p/django-jython/wiki/Install .. _`database backends`: http://code.google.com/p/django-jython/wiki/DatabaseBackends diff --git a/docs/howto/legacy-databases.txt b/docs/howto/legacy-databases.txt index b2aa7e4ea624..2121871fa2d1 100644 --- a/docs/howto/legacy-databases.txt +++ b/docs/howto/legacy-databases.txt @@ -1,5 +1,3 @@ -.. _howto-legacy-databases: - ========================================= Integrating Django with a legacy database ========================================= @@ -9,7 +7,7 @@ possible to integrate it into legacy databases. Django includes a couple of utilities to automate as much of this process as possible. This document assumes you know the Django basics, as covered in the -:ref:`tutorial `. +:doc:`tutorial `. Once you've got Django set up, you'll follow this general process to integrate with an existing database. diff --git a/docs/howto/outputting-csv.txt b/docs/howto/outputting-csv.txt index 234454c2657f..46e111d8afee 100644 --- a/docs/howto/outputting-csv.txt +++ b/docs/howto/outputting-csv.txt @@ -1,5 +1,3 @@ -.. _howto-outputting-csv: - ========================== Outputting CSV with Django ========================== @@ -58,12 +56,35 @@ mention: about escaping strings with quotes or commas in them. Just pass ``writerow()`` your raw strings, and it'll do the right thing. +Handling Unicode +~~~~~~~~~~~~~~~~ + +Python's ``csv`` module does not support Unicode input. Since Django uses +Unicode internally this means strings read from sources such as +:class:`~django.http.HttpRequest` are potentially problematic. There are a few +options for handling this: + + * Manually encode all Unicode objects to a compatible encoding. + + * Use the ``UnicodeWriter`` class provided in the `csv module's examples + section`_. + + * Use the `python-unicodecsv module`_, which aims to be a drop-in + replacement for ``csv`` that gracefully handles Unicode. + +For more information, see the Python `CSV File Reading and Writing`_ +documentation. + +.. _`csv module's examples section`: http://docs.python.org/library/csv.html#examples +.. _`python-unicodecsv module`: https://github.com/jdunck/python-unicodecsv +.. _`CSV File Reading and Writing`: http://docs.python.org/library/csv.html + Using the template system ========================= -Alternatively, you can use the :ref:`Django template system ` -to generate CSV. This is lower-level than using the convenient CSV, but the -solution is presented here for completeness. +Alternatively, you can use the :doc:`Django template system ` +to generate CSV. This is lower-level than using the convenient Python ``csv`` +module, but the solution is presented here for completeness. The idea here is to pass a list of items to your template, and have the template output the commas in a :ttag:`for` loop. @@ -113,4 +134,4 @@ Other text-based formats Notice that there isn't very much specific to CSV here -- just the specific output format. You can use either of these techniques to output any text-based format you can dream of. You can also use a similar technique to generate -arbitrary binary data; see :ref:`howto-outputting-pdf` for an example. +arbitrary binary data; see :doc:`/howto/outputting-pdf` for an example. diff --git a/docs/howto/outputting-pdf.txt b/docs/howto/outputting-pdf.txt index 94acab83112e..67950d03f248 100644 --- a/docs/howto/outputting-pdf.txt +++ b/docs/howto/outputting-pdf.txt @@ -1,5 +1,3 @@ -.. _howto-outputting-pdf: - =========================== Outputting PDFs with Django =========================== @@ -103,7 +101,11 @@ cStringIO_ library as a temporary holding place for your PDF file. The cStringIO library provides a file-like object interface that is particularly efficient. Here's the above "Hello World" example rewritten to use ``cStringIO``:: - from cStringIO import StringIO + # Fall back to StringIO in environments where cStringIO is not available + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO from reportlab.pdfgen import canvas from django.http import HttpResponse @@ -154,5 +156,5 @@ Other formats Notice that there isn't a lot in these examples that's PDF-specific -- just the bits using ``reportlab``. You can use a similar technique to generate any arbitrary format that you can find a Python library for. Also see -:ref:`howto-outputting-csv` for another example and some techniques you can use +:doc:`/howto/outputting-csv` for another example and some techniques you can use when generated text-based formats. diff --git a/docs/howto/static-files.txt b/docs/howto/static-files.txt index f93a4e9ba42c..c3692d527173 100644 --- a/docs/howto/static-files.txt +++ b/docs/howto/static-files.txt @@ -1,5 +1,3 @@ -.. _howto-static-files: - ========================= How to serve static files ========================= @@ -42,7 +40,7 @@ Here's the formal definition of the :func:`~django.views.static.serve` view: .. function:: def serve(request, path, document_root, show_indexes=False) -To use it, just put this in your :ref:`URLconf `:: +To use it, just put this in your :doc:`URLconf `:: (r'^site_media/(?P.*)$', 'django.views.static.serve', {'document_root': '/path/to/media'}), @@ -71,7 +69,7 @@ required. For example, if we have a line in ``settings.py`` that says:: STATIC_DOC_ROOT = '/path/to/media' -...we could write the above :ref:`URLconf ` entry as:: +...we could write the above :doc:`URLconf ` entry as:: from django.conf import settings ... diff --git a/docs/index.txt b/docs/index.txt index aae2e27cb676..9dd1c5fe7556 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -12,16 +12,16 @@ Getting help Having trouble? We'd like to help! -* Try the :ref:`FAQ ` -- it's got answers to many common questions. +* Try the :doc:`FAQ ` -- it's got answers to many common questions. * Looking for specific information? Try the :ref:`genindex`, :ref:`modindex` or - the :ref:`detailed table of contents `. + the :doc:`detailed table of contents `. * Search for information in the `archives of the django-users mailing list`_, or `post a question`_. * Ask a question in the `#django IRC channel`_, or search the `IRC logs`_ to see - if its been asked before. + if it's been asked before. * Report bugs with Django in our `ticket tracker`_. @@ -35,179 +35,180 @@ First steps =========== * **From scratch:** - :ref:`Overview ` | - :ref:`Installation ` + :doc:`Overview ` | + :doc:`Installation ` * **Tutorial:** - :ref:`Part 1 ` | - :ref:`Part 2 ` | - :ref:`Part 3 ` | - :ref:`Part 4 ` + :doc:`Part 1 ` | + :doc:`Part 2 ` | + :doc:`Part 3 ` | + :doc:`Part 4 ` The model layer =============== * **Models:** - :ref:`Model syntax ` | - :ref:`Field types ` | - :ref:`Meta options ` + :doc:`Model syntax ` | + :doc:`Field types ` | + :doc:`Meta options ` * **QuerySets:** - :ref:`Executing queries ` | - :ref:`QuerySet method reference ` + :doc:`Executing queries ` | + :doc:`QuerySet method reference ` * **Model instances:** - :ref:`Instance methods ` | - :ref:`Accessing related objects ` + :doc:`Instance methods ` | + :doc:`Accessing related objects ` * **Advanced:** - :ref:`Managers ` | - :ref:`Raw SQL ` | - :ref:`Transactions ` | - :ref:`Aggregation ` | - :ref:`Custom fields ` | - :ref:`Multiple databases ` + :doc:`Managers ` | + :doc:`Raw SQL ` | + :doc:`Transactions ` | + :doc:`Aggregation ` | + :doc:`Custom fields ` | + :doc:`Multiple databases ` * **Other:** - :ref:`Supported databases ` | - :ref:`Legacy databases ` | - :ref:`Providing initial data ` | - :ref:`Optimize database access ` + :doc:`Supported databases ` | + :doc:`Legacy databases ` | + :doc:`Providing initial data ` | + :doc:`Optimize database access ` The template layer ================== * **For designers:** - :ref:`Syntax overview ` | - :ref:`Built-in tags and filters ` + :doc:`Syntax overview ` | + :doc:`Built-in tags and filters ` * **For programmers:** - :ref:`Template API ` | - :ref:`Custom tags and filters ` + :doc:`Template API ` | + :doc:`Custom tags and filters ` The view layer ============== * **The basics:** - :ref:`URLconfs ` | - :ref:`View functions ` | - :ref:`Shortcuts ` + :doc:`URLconfs ` | + :doc:`View functions ` | + :doc:`Shortcuts ` | + :doc:`Decorators ` - * **Reference:** :ref:`Request/response objects ` + * **Reference:** :doc:`Request/response objects ` * **File uploads:** - :ref:`Overview ` | - :ref:`File objects ` | - :ref:`Storage API ` | - :ref:`Managing files ` | - :ref:`Custom storage ` + :doc:`Overview ` | + :doc:`File objects ` | + :doc:`Storage API ` | + :doc:`Managing files ` | + :doc:`Custom storage ` * **Generic views:** - :ref:`Overview` | - :ref:`Built-in generic views` + :doc:`Overview` | + :doc:`Built-in generic views` * **Advanced:** - :ref:`Generating CSV ` | - :ref:`Generating PDF ` + :doc:`Generating CSV ` | + :doc:`Generating PDF ` * **Middleware:** - :ref:`Overview ` | - :ref:`Built-in middleware classes ` + :doc:`Overview ` | + :doc:`Built-in middleware classes ` Forms ===== * **The basics:** - :ref:`Overview ` | - :ref:`Form API ` | - :ref:`Built-in fields ` | - :ref:`Built-in widgets ` + :doc:`Overview ` | + :doc:`Form API ` | + :doc:`Built-in fields ` | + :doc:`Built-in widgets ` * **Advanced:** - :ref:`Forms for models ` | - :ref:`Integrating media ` | - :ref:`Formsets ` | - :ref:`Customizing validation ` + :doc:`Forms for models ` | + :doc:`Integrating media ` | + :doc:`Formsets ` | + :doc:`Customizing validation ` * **Extras:** - :ref:`Form preview ` | - :ref:`Form wizard ` + :doc:`Form preview ` | + :doc:`Form wizard ` The development process ======================= * **Settings:** - :ref:`Overview ` | - :ref:`Full list of settings ` + :doc:`Overview ` | + :doc:`Full list of settings ` * **Exceptions:** - :ref:`Overview ` + :doc:`Overview ` * **django-admin.py and manage.py:** - :ref:`Overview ` | - :ref:`Adding custom commands ` + :doc:`Overview ` | + :doc:`Adding custom commands ` - * **Testing:** :ref:`Overview ` + * **Testing:** :doc:`Overview ` * **Deployment:** - :ref:`Overview ` | - :ref:`Apache/mod_wsgi ` | - :ref:`Apache/mod_python ` | - :ref:`FastCGI/SCGI/AJP ` | - :ref:`Apache authentication ` | - :ref:`Serving static files ` | - :ref:`Tracking code errors by e-mail ` + :doc:`Overview ` | + :doc:`Apache/mod_wsgi ` | + :doc:`Apache/mod_python ` | + :doc:`FastCGI/SCGI/AJP ` | + :doc:`Apache authentication ` | + :doc:`Serving static files ` | + :doc:`Tracking code errors by e-mail ` Other batteries included ======================== - * :ref:`Admin site ` | :ref:`Admin actions ` - * :ref:`Authentication ` - * :ref:`Cache system ` - * :ref:`Conditional content processing ` - * :ref:`Comments ` | :ref:`Moderation ` | :ref:`Custom comments ` - * :ref:`Content types ` - * :ref:`Cross Site Request Forgery protection ` - * :ref:`Databrowse ` - * :ref:`E-mail (sending) ` - * :ref:`Flatpages ` - * :ref:`GeoDjango ` - * :ref:`Humanize ` - * :ref:`Internationalization ` - * :ref:`Jython support ` - * :ref:`"Local flavor" ` - * :ref:`Messages ` - * :ref:`Pagination ` - * :ref:`Redirects ` - * :ref:`Serialization ` - * :ref:`Sessions ` - * :ref:`Signals ` - * :ref:`Sitemaps ` - * :ref:`Sites ` - * :ref:`Syndication feeds (RSS/Atom) ` - * :ref:`Unicode in Django ` - * :ref:`Web design helpers ` - * :ref:`Validators ` + * :doc:`Admin site ` | :doc:`Admin actions ` | :doc:`Admin documentation generator` + * :doc:`Authentication ` + * :doc:`Cache system ` + * :doc:`Conditional content processing ` + * :doc:`Comments ` | :doc:`Moderation ` | :doc:`Custom comments ` + * :doc:`Content types ` + * :doc:`Cross Site Request Forgery protection ` + * :doc:`Databrowse ` + * :doc:`E-mail (sending) ` + * :doc:`Flatpages ` + * :doc:`GeoDjango ` + * :doc:`Humanize ` + * :doc:`Internationalization ` + * :doc:`Jython support ` + * :doc:`"Local flavor" ` + * :doc:`Messages ` + * :doc:`Pagination ` + * :doc:`Redirects ` + * :doc:`Serialization ` + * :doc:`Sessions ` + * :doc:`Signals ` + * :doc:`Sitemaps ` + * :doc:`Sites ` + * :doc:`Syndication feeds (RSS/Atom) ` + * :doc:`Unicode in Django ` + * :doc:`Web design helpers ` + * :doc:`Validators ` The Django open-source project ============================== * **Community:** - :ref:`How to get involved ` | - :ref:`The release process ` | - :ref:`Team of committers ` | - :ref:`The Django source code repository ` + :doc:`How to get involved ` | + :doc:`The release process ` | + :doc:`Team of committers ` | + :doc:`The Django source code repository ` * **Design philosophies:** - :ref:`Overview ` + :doc:`Overview ` * **Documentation:** - :ref:`About this documentation ` + :doc:`About this documentation ` * **Third-party distributions:** - :ref:`Overview ` + :doc:`Overview ` * **Django over time:** - :ref:`API stability ` | - :ref:`Release notes and upgrading instructions ` | - :ref:`Deprecation Timeline ` + :doc:`API stability ` | + :doc:`Release notes and upgrading instructions ` | + :doc:`Deprecation Timeline ` diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index d2eb80c7102b..ecda1d5a9965 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -1,5 +1,3 @@ -.. _internals-committers: - ================= Django committers ================= @@ -22,10 +20,10 @@ Journal-World`_ of Lawrence, Kansas, USA. Adrian lives in Chicago, USA. `Simon Willison`_ - Simon is a well-respected web developer from England. He had a one-year + Simon is a well-respected Web developer from England. He had a one-year internship at World Online, during which time he and Adrian developed Django from scratch. The most enthusiastic Brit you'll ever meet, he's passionate - about best practices in web development and maintains a well-read + about best practices in Web development and maintains a well-read `web-development blog`_. Simon lives in Brighton, England. @@ -35,13 +33,13 @@ Journal-World`_ of Lawrence, Kansas, USA. around Django and related open source technologies. A good deal of Jacob's work time is devoted to working on Django. Jacob previously worked at World Online, where Django was invented, where he was the lead developer of - Ellington, a commercial web publishing platform for media companies. + Ellington, a commercial Web publishing platform for media companies. Jacob lives in Lawrence, Kansas, USA. `Wilson Miner`_ Wilson's design-fu is what makes Django look so nice. He designed the - website you're looking at right now, as well as Django's acclaimed admin + Web site you're looking at right now, as well as Django's acclaimed admin interface. Wilson is the designer for EveryBlock_. Wilson lives in San Francisco, USA. @@ -91,7 +89,7 @@ and have free rein to hack on all parts of Django. 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 simulation frameworks. Over time, mostly through work with Django, he's - become more involved in web development. + become more involved in Web development. Russell has helped with several major aspects of Django, including a couple major internal refactorings, creation of the test system, and more. @@ -103,7 +101,7 @@ and have free rein to hack on all parts of Django. Joseph Kocherhans Joseph is currently a developer at EveryBlock_, and previously worked for - the Lawrence Journal-World where he built most of the backend for the their + the Lawrence Journal-World where he built most of the backend for their Marketplace site. He often disappears for several days into the woods, attempts to teach himself computational linguistics, and annoys his neighbors with his Charango_ playing. @@ -136,10 +134,10 @@ Joseph Kocherhans `Brian Rosner`_ Brian is currently the tech lead at Eldarion_ managing and developing - Django / Pinax_ based websites. He enjoys learning more about programming + 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. - + 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 the admin and forms system. @@ -182,7 +180,7 @@ Karen Tracey Karen has a background in distributed operating systems (graduate school), communications software (industry) and crossword puzzle construction (freelance). The last of these brought her to Django, in late 2006, when - she set out to put a web front-end on her crossword puzzle database. + she set out to put a Web front-end on her crossword puzzle database. That done, she stuck around in the community answering questions, debugging problems, etc. -- because coding puzzles are as much fun as word puzzles. @@ -192,7 +190,7 @@ Karen Tracey Jannis graduated in media design from `Bauhaus-University Weimar`_, is the author of a number of pluggable Django apps and likes to contribute to Open Source projects like Pinax_. He currently works as - a freelance web developer and designer. + a freelance Web developer and designer. Jannis lives in Berlin, Germany. @@ -206,20 +204,77 @@ Karen Tracey since 1998 and Django since 2006. He serves on the board of the Python Software Foundation and is currently on a leave of absence from a PhD in linguistics. - - James currently lives in Boston, MA, USA but originally hails from - Perth, Western Australia where he attended the same high school as + + James currently lives in Boston, MA, USA but originally hails from + Perth, Western Australia where he attended the same high school as Russell Keith-Magee. .. _James Tauber: http://jtauber.com/ +`Alex Gaynor`_ + Alex is a student at Rensselaer Polytechnic Institute, and is also an + independent contractor. 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. + + Alex lives in Chicago, IL, but spends most of his time in Troy, NY. + +.. _Alex Gaynor: http://alexgaynor.net + +`Andrew Godwin`_ + Andrew is a freelance Python developer and tinkerer, and has been + developing against Django since 2007. He graduated from Oxford University + with a degree in Computer Science, and has become most well known + in the Django community for his work on South, the schema migrations + library. + + Andrew lives in London, UK. + +.. _Andrew Godwin: http://www.aeracode.org/ + +`Carl Meyer`_ + Carl has been working with Django since 2007 (long enough to remember + queryset-refactor, but not magic-removal), and works as a freelance + developer with OddBird_ and Eldarion_. He became a Django contributor by + accident, because fixing bugs is more interesting than working around + them. + + Carl lives in Elkhart, IN, USA. + +.. _Carl Meyer: http://www.oddbird.net/about/#hcard-carl +.. _OddBird: http://www.oddbird.net/ + +Ramiro Morales + Ramiro has been reading Django source code and submitting patches since + mid-2006 after researching for a Python Web tool with matching awesomeness + and being pointed to it by an old ninja. + + A software developer in the electronic transactions industry, he is a + living proof of the fact that anyone with enough enthusiasm can contribute + to Django, learning a lot and having fun in the process. + + Ramiro lives in Córdoba, Argentina. + +`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 + triager, he is still often found answering questions in the #django IRC + channel. + + Chris lives in Napier, New Zealand (adding to the pool of Oceanic core + developers). He works remotely as a developer for `Lincoln Loop`_. + +.. _Chris Beaven: http://smileychris.com/ +.. _Lincoln Loop: http://lincolnloop.com/ + Specialists ----------- `James Bennett`_ James is Django's release manager; he also contributes to the documentation. - James came to web development from philosophy when he discovered + 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, where he works for the Journal-World developing Ellington. He `keeps a blog`_, has @@ -237,7 +292,7 @@ Matt Boersma Matt is also responsible for Django's Oracle support. Jeremy Dunck - Jeremy the lead developer of Pegasus News, a personalized local site based + Jeremy is the lead developer of Pegasus News, a personalized local site based in Dallas, Texas. An early contributor to Greasemonkey and Django, he sees technology as a tool for communication and access to knowledge. @@ -246,6 +301,35 @@ Jeremy Dunck Jeremy lives in Dallas, Texas, USA. +`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. + Developers Emeritus =================== diff --git a/docs/internals/contributing.txt b/docs/internals/contributing.txt index c555f205b10b..390ce544da3a 100644 --- a/docs/internals/contributing.txt +++ b/docs/internals/contributing.txt @@ -1,5 +1,3 @@ -.. _internals-contributing: - ====================== Contributing to Django ====================== @@ -17,7 +15,9 @@ of the community, so there are many ways you can help Django's development: served up. * Submit patches for new and/or fixed behavior. Please read `Submitting - patches`_, below, for details on how to submit a patch. + patches`_, below, for details on how to submit a patch. If you're looking + for an easy way to start contributing to Django have a look at the + `easy-pickings`_ tickets. * Join the `django-developers`_ mailing list and share your ideas for how to improve Django. We're always open to suggestions, although we're @@ -42,7 +42,7 @@ amount of overhead involved in working with any bug tracking system, so your help in keeping our ticket tracker as useful as possible is appreciated. In particular: - * **Do** read the :ref:`FAQ ` to see if your issue might be a well-known question. + * **Do** read the :doc:`FAQ ` to see if your issue might be a well-known question. * **Do** `search the tracker`_ to see if your issue has already been filed. @@ -145,7 +145,11 @@ and time availability), claim it by following these steps: * Claim the ticket by clicking the radio button next to "Accept ticket" near the bottom of the page, then clicking "Submit changes." +If you have an account but have forgotten your password, you can reset it +using the `password reset page`_. + .. _Create an account: http://www.djangoproject.com/accounts/register/ +.. _password reset page: http://www.djangoproject.com/accounts/password/reset/ Ticket claimers' responsibility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -154,11 +158,9 @@ Once you've claimed a ticket, you have a responsibility to work on that ticket in a reasonably timely fashion. If you don't have time to work on it, either unclaim it or don't claim it in the first place! -Ticket triagers go through the list of claimed tickets from time to -time, checking whether any progress has been made. If there's no sign of -progress on a particular claimed ticket for a week or two, a triager may ask -you to relinquish the ticket claim so that it's no longer monopolized and -somebody else can claim it. +If there's no sign of progress on a particular claimed ticket for a week or +two, another developer may ask you to relinquish the ticket claim so that it's +no longer monopolized and somebody else can claim it. If you've claimed a ticket and it's taking a long time (days or weeks) to code, keep everybody updated by posting comments on the ticket. If you don't provide @@ -181,20 +183,21 @@ Patch style * Make sure your code matches our `coding style`_. * Submit patches in the format returned by the ``svn diff`` command. - An exception is for code changes that are described more clearly in plain - English than in code. Indentation is the most common example; it's hard to - read patches when the only difference in code is that it's indented. + An exception is for code changes that are described more clearly in + plain English than in code. Indentation is the most common example; it's + hard to read patches when the only difference in code is that it's + indented. Patches in ``git diff`` format are also acceptable. * When creating patches, always run ``svn diff`` from the top-level ``trunk`` directory -- i.e., the one that contains ``django``, ``docs``, - ``tests``, ``AUTHORS``, etc. This makes it easy for other people to apply - your patches. + ``tests``, ``AUTHORS``, etc. This makes it easy for other people to + apply your patches. - * Attach patches to a ticket in the `ticket tracker`_, using the "attach file" - button. Please *don't* put the patch in the ticket description or comment - unless it's a single line patch. + * Attach patches to a ticket in the `ticket tracker`_, using the "attach + file" button. Please *don't* put the patch in the ticket description + or comment unless it's a single line patch. * Name the patch file with a ``.diff`` extension; this will let the ticket tracker apply correct syntax highlighting, which is quite helpful. @@ -205,11 +208,12 @@ Patch style * The code required to fix a problem or add a feature is an essential part of a patch, but it is not the only part. A good patch should also include - a regression test to validate the behavior that has been fixed (and prevent - the problem from arising again). + a regression test to validate the behavior that has been fixed + (and prevent the problem from arising again). - * If the code associated with a patch adds a new feature, or modifies behavior - of an existing feature, the patch should also contain documentation. + * If the code associated with a patch adds a new feature, or modifies + behavior of an existing feature, the patch should also contain + documentation. Non-trivial patches ------------------- @@ -229,8 +233,8 @@ the `required details`_. A number of tickets have patches, but those patches don't meet all the requirements of a `good patch`_. One way to help out is to *triage* bugs that have been reported by other -users. A couple of dedicated volunteers work on this regularly, but more help -is always appreciated. +users. The core team--as well as many community members--work on this +regularly, but more help is always appreciated. Most of the workflow is based around the concept of a ticket's "triage stage". This stage describes where in its lifetime a given ticket is at any time. @@ -244,22 +248,28 @@ Since a picture is worth a thousand words, let's start there: :width: 590 :alt: Django's ticket workflow -We've got two official roles here: +We've got two roles in this diagram: - * Core developers: people with commit access who make the big decisions - and write the bulk of the code. + * Core developers: people with commit access who are responsible for + making the big decisions, writing large portions of the code and + integrating the contributions of the community. - * Ticket triagers: trusted community members with a proven history of - working with the Django community. As a result of this history, they - have been entrusted by the core developers to make some of the smaller - decisions about tickets. + * Ticket triagers: anyone in the Django community who chooses to + become involved in Django's development process. Our Trac installation + is :ref:`intentionally left open to the public + `, and anyone can triage tickets. + Django is a community project, and we encourage `triage by the + community`_. + +Triage stages +------------- Second, note the five triage stages: - 1. A ticket starts as "Unreviewed", meaning that nobody has examined + 1. A ticket starts as **Unreviewed**, meaning that nobody has examined the ticket. - 2. "Design decision needed" means "this concept requires a design + 2. **Design decision needed** means "this concept requires a design decision," which should be discussed either in the ticket comments or on `django-developers`_. The "Design decision needed" step will generally only be used for feature requests. It can also be used for issues @@ -268,29 +278,29 @@ Second, note the five triage stages: standard) skip this step and move straight to "Accepted". 3. Once a ticket is ruled to be approved for fixing, it's moved into the - "Accepted" stage. This stage is where all the real work gets done. + **Accepted** stage. This stage is where all the real work gets done. - 4. In some cases, a ticket might get moved to the "Someday/Maybe" state. + 4. In some cases, a ticket might get moved to the **Someday/Maybe** state. This means the ticket is an enhancement request that we might consider adding to the framework if an excellent patch is submitted. These tickets are not a high priority. - 5. If a ticket has an associated patch (see below), a triager will review - the patch. If the patch is complete, it'll be marked as "ready for - checkin" so that a core developer knows to review and check in the - patches. + 5. If a ticket has an associated patch (see below), it will be reviewed + by the community. If the patch is complete, it'll be marked as **Ready + for checkin** so that a core developer knows to review and commit the + patch. The second part of this workflow involves a set of flags the describe what the ticket has or needs in order to be "ready for checkin": "Has patch" This means the ticket has an associated patch_. These will be - reviewed by the triage team to see if the patch is "good". + reviewed to see if the patch is "good". "Needs documentation" This flag is used for tickets with patches that need associated documentation. Complete documentation of features is a prerequisite - before we can check a fix into the codebase. + before we can check them into the codebase. "Needs tests" This flags the patch as needing associated unit tests. Again, this is a @@ -299,12 +309,24 @@ ticket has or needs in order to be "ready for checkin": "Patch needs improvement" This flag means that although the ticket *has* a patch, it's not quite ready for checkin. This could mean the patch no longer applies - cleanly, or that the code doesn't live up to our standards. + cleanly, there is a flaw in the implementation, or that the code + doesn't meet our standards. + +.. seealso:: + + The :ref:`contributing howto guide ` has a detailed + explanation of each of the triage stages and how the triage process works in + Trac. + +.. _ticket-resolutions: + +Ticket Resolutions +------------------ A ticket can be resolved in a number of ways: "fixed" - Used by one of the core developers once a patch has been rolled into + Used by the core developers once a patch has been rolled into Django and the issue is fixed. "invalid" @@ -317,8 +339,10 @@ A ticket can be resolved in a number of ways: "wontfix" Used when a core developer decides that this request is not appropriate for consideration in Django. This is usually chosen after - discussion in the ``django-developers`` mailing list, and you should - feel free to join in when it's something you care about. + discussion in the ``django-developers`` mailing list. Feel free to + start or join in discussions of "wontfix" tickets on the mailing list, + but please do not reopen tickets closed as "wontfix" by core + developers. "duplicate" Used when another ticket covers the same issue. By closing duplicate @@ -328,43 +352,58 @@ A ticket can be resolved in a number of ways: Used when the ticket doesn't contain enough detail to replicate the original bug. + "needsinfo" + Used when the ticket does not contain enough information to replicate + the reported issue but is potentially still valid. The ticket + should be reopened when more information is supplied. + If you believe that the ticket was closed in error -- because you're still having the issue, or it's popped up somewhere else, or the triagers have --- made a mistake, please reopen the ticket and tell us why. Please do not -reopen tickets that have been marked as "wontfix" by core developers. +made a mistake -- please reopen the ticket and provide further information. +Please do not reopen tickets that have been marked as "wontfix" by core +developers. + +.. seealso:: + + For more information on what to do when closing a ticket, please see the + :ref:`contributing howto guide `. .. _required details: `Reporting bugs`_ .. _good patch: `Patch style`_ +.. _triage by the community: `Triage by the general community`_ .. _patch: `Submitting patches`_ Triage by the general community ------------------------------- -Although the core developers and ticket triagers make the big decisions in -the ticket triage process, there's also a lot that general community -members can do to help the triage process. In particular, you can help out by: +Although the core developers make the big decisions in the ticket triage +process, there's a lot that general community members can do to help the +triage process. In particular, you can help out by: * Closing "Unreviewed" tickets as "invalid", "worksforme" or "duplicate." * Promoting "Unreviewed" tickets to "Design decision needed" if a design decision needs to be made, or "Accepted" in case of obvious bugs. - * Correcting the "Needs tests", "Needs documentation", or "Has patch" flags - for tickets where they are incorrectly set. + * Correcting the "Needs tests", "Needs documentation", or "Has patch" + flags for tickets where they are incorrectly set. + + * Adding the `easy-pickings`_ keyword to tickets that are small and + relatively straightforward. * Checking that old tickets are still valid. If a ticket hasn't seen any activity in a long time, it's possible that the problem has been fixed but the ticket hasn't yet been closed. - * Contacting the owners of tickets that have been claimed but have not seen - any recent activity. If the owner doesn't respond after a week or so, - remove the owner's claim on the ticket. + * Contacting the owners of tickets that have been claimed but have not + seen any recent activity. If the owner doesn't respond after a week + or so, remove the owner's claim on the ticket. - * Identifying trends and themes in the tickets. If there a lot of bug reports - about a particular part of Django, it may indicate we should consider - refactoring that part of the code. If a trend is emerging, you should - raise it for discussion (referencing the relevant tickets) on - `django-developers`_. + * Identifying trends and themes in the tickets. If there a lot of bug + reports about a particular part of Django, it may indicate we should + consider refactoring that part of the code. If a trend is emerging, + you should raise it for discussion (referencing the relevant tickets) + on `django-developers`_. However, we do ask the following of all general community members working in the ticket database: @@ -373,17 +412,19 @@ the ticket database: make the final determination of the fate of a ticket, usually after consultation with the community. - * Please **don't** promote tickets to "Ready for checkin" unless they are - *trivial* changes -- for example, spelling mistakes or broken links in - documentation. + * Please **don't** promote your own tickets to "Ready for checkin". You + may mark other people's tickets which you've reviewed as "Ready for + checkin", but you should get at minimum one other community member to + review a patch that you submit. * Please **don't** reverse a decision that has been made by a core - developer. If you disagree with a discussion that has been made, + developer. If you disagree with a decision that has been made, please post a message to `django-developers`_. - * Please be conservative in your actions. If you're unsure if you should - be making a change, don't make the change -- leave a comment with your - concerns on the ticket, or post a message to `django-developers`_. + * If you're unsure if you should be making a change, don't make the change + but instead leave a comment with your concerns on the ticket, or + post a message to `django-developers`_. It's okay to be unsure, but + your input is still valuable. .. _contributing-translations: @@ -394,7 +435,7 @@ Various parts of Django, such as the admin site and validation error messages, are internationalized. This means they display different text depending on a user's language setting. For this, Django uses the same internationalization infrastructure available to Django applications described in the -:ref:`i18n documentation`. +:doc:`i18n documentation`. These translations are contributed by Django users worldwide. If you find an incorrect translation, or if you'd like to add a language that isn't yet @@ -405,7 +446,7 @@ translated, here's what to do: * Make sure you read the notes about :ref:`specialties-of-django-i18n`. * Create translations using the methods described in the - :ref:`localization documentation `. For this + :doc:`localization documentation `. For this you will use the ``django-admin.py makemessages`` tool. In this particular case it should be run from the top-level ``django`` directory of the Django source tree. @@ -531,8 +572,8 @@ Please follow these coding standards when writing code for inclusion in Django: * Use ``InitialCaps`` for class names (or for factory functions that return classes). - * Mark all strings for internationalization; see the :ref:`i18n - documentation ` for details. + * Mark all strings for internationalization; see the :doc:`i18n + documentation ` for details. * In docstrings, use "action words" such as:: @@ -694,14 +735,14 @@ General improvements, or other changes to the APIs that should be emphasized should use the ".. versionchanged:: X.Y" directive (with the same format as the ``versionadded`` mentioned above. -There's a full page of information about the :ref:`Django documentation -system ` that you should read prior to working on the +There's a full page of information about the :doc:`Django documentation +system ` that you should read prior to working on the documentation. -Guidelines for ReST files +Guidelines for reST files ------------------------- -These guidelines regulate the format of our ReST documentation: +These guidelines regulate the format of our reST documentation: * In section titles, capitalize only initial words and proper nouns. @@ -810,6 +851,44 @@ repository: Subversion and Trac so that any commit message in that format will automatically post a comment to the appropriate ticket. +Reverting commits +----------------- + +Nobody's perfect; mistakes will be committed. When a mistaken commit is +discovered, please follow these guidelines: + + * Try very hard to ensure that mistakes don't happen. Just because we + have a reversion policy doesn't relax your responsibility to aim for + the highest quality possible. Really: double-check your work before + you commit it in the first place! + + * If possible, have the original author revert his/her own commit. + + * Don't revert another author's changes without permission from the + original author. + + * If the original author can't be reached (within a reasonable amount + of time -- a day or so) and the problem is severe -- crashing bug, + major test failures, etc -- then ask for objections on django-dev + then revert if there are none. + + * If the problem is small (a feature commit after feature freeze, + say), wait it out. + + * If there's a disagreement between the committer and the + reverter-to-be then try to work it out on the `django-developers`_ + mailing list. If an agreement can't be reached then it should + be put to a vote. + + * If the commit introduced a confirmed, disclosed security + vulnerability then the commit may be reverted immediately without + permission from anyone. + + * The release branch maintainer may back out commits to the release + branch without permission if the commit breaks the release branch. + +.. _unit-tests: + Unit tests ========== @@ -825,20 +904,49 @@ The tests cover: We appreciate any and all contributions to the test suite! The Django tests all use the testing infrastructure that ships with Django for -testing applications. See :ref:`Testing Django applications ` +testing applications. See :doc:`Testing Django applications ` for an explanation of how to write new tests. +.. _running-unit-tests: + Running the unit tests ---------------------- -To run the tests, ``cd`` to the ``tests/`` directory and type: +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: + +.. 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``. For +more details on how to do this, read `Pointing Python at the new +Django version`_ below. + +Using another ``settings`` module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The included settings module allows you to run the test suite using +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: .. code-block:: bash ./runtests.py --settings=path.to.django.settings -Yes, the unit tests need a settings module, but only for database connection -info. Your :setting:`DATABASES` setting needs to define two databases: +The :setting:`DATABASES` setting in this test settings module needs to define +two databases: * A ``default`` database. This database should use the backend that you want to use for primary testing @@ -849,38 +957,8 @@ info. Your :setting:`DATABASES` setting needs to define two databases: want. It doesn't need to use the same backend as the ``default`` database (although it can use the same backend if you want to). -If you're using the SQLite database backend, you need to define -:setting:`ENGINE` for both databases, plus a -:setting:`TEST_NAME` for the ``other`` database. The -following is a minimal settings file that can be used to test SQLite:: - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3' - }, - 'other': { - 'ENGINE': 'django.db.backends.sqlite3', - 'TEST_NAME': 'other_db' - } - } - -As a convenience, this settings file is included in your Django -distribution. It is called ``test_sqlite``, and is included in -the ``tests`` directory. This allows you to get started running -the tests against the sqlite database without doing anything on -your filesystem. However it should be noted that running against -other database backends is recommended for certain types of test -cases. - -To run the tests with this included settings file, ``cd`` -to the ``tests/`` directory and type: - -.. code-block:: bash - - ./runtests.py --settings=test_sqlite - -If you're using another backend, you will need to provide other details for -each database: +If you're using a backend that isn't SQLite, you will need to provide other +details for each database: * The :setting:`USER` option for each of your databases needs to specify an existing user account for the database. @@ -900,6 +978,40 @@ character set. If your database server doesn't use UTF-8 as a default charset, you will need to include a value for ``TEST_CHARSET`` in the settings dictionary for the applicable database. +Running only some of the tests +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django's entire test suite takes a few minutes to run. To run a subset of the +unit tests, append the names of the test modules to the ``runtests.py`` +command line. + +As an example, if you'd like to only run tests for generic relations and +internationalization, type: + +.. code-block:: bash + + ./runtests.py --settings=path.to.settings generic_relations i18n + +See the list of directories in ``tests/modeltests`` and +``tests/regressiontests`` for module names. + +If you just want to run a particular class of tests, you can specify a list of +paths to individual test classes. For example, to run the ``TranslationTests`` +of the ``i18n`` module, type: + +.. code-block:: bash + + ./runtests.py --settings=path.to.settings i18n.TranslationTests + +You can specify an individual test like this: + +.. code-block:: bash + + ./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects + +Running all the tests +~~~~~~~~~~~~~~~~~~~~~ + If you want to run the full suite of tests, you'll need to install a number of dependencies: @@ -928,19 +1040,6 @@ associated tests will be skipped. .. _cmemcached: http://gijsbert.org/cmemcache/index.html .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html -To run a subset of the unit tests, append the names of the test modules to the -``runtests.py`` command line. See the list of directories in -``tests/modeltests`` and ``tests/regressiontests`` for module names. - -As an example, if Django is not in your ``PYTHONPATH``, you placed -``settings.py`` in the ``tests/`` directory, and you'd like to only run tests -for generic relations and internationalization, type: - -.. code-block:: bash - - PYTHONPATH=`pwd`/.. - ./runtests.py --settings=settings generic_relations i18n - Contrib apps ------------ @@ -1013,8 +1112,8 @@ for feature branches: public, please add the branch to the `Django branches`_ wiki page. 2. Feature branches using SVN have a higher bar. If you want a branch in SVN - itself, you'll need a "mentor" among the :ref:`core committers - `. This person is responsible for actually creating + itself, you'll need a "mentor" among the :doc:`core committers + `. This person is responsible for actually creating the branch, monitoring your process (see below), and ultimately merging the branch into trunk. @@ -1157,11 +1256,8 @@ file. Then copy the branch's version of the ``django`` directory into .. _path file: http://docs.python.org/library/site.html -Deciding on features -==================== - -Once a feature's been requested and discussed, eventually we'll have a decision -about whether to include the feature or drop it. +How we make decisions +===================== Whenever possible, we strive for a rough consensus. To that end, we'll often have informal votes on `django-developers`_ about a feature. In these votes we @@ -1181,23 +1277,44 @@ Although these votes on django-developers are informal, they'll be taken very seriously. After a suitable voting period, if an obvious consensus arises we'll follow the votes. -However, consensus is not always possible. Tough decisions will be discussed by -all full committers and finally decided by the Benevolent Dictators for Life, -Adrian and Jacob. +However, consensus is not always possible. If consensus cannot be reached, or +if the discussion towards a consensus fizzles out without a concrete decision, +we use a more formal process. + +Any core committer (see below) may call for a formal vote using the same +voting mechanism above. A proposition will be considered carried by the core team +if: + + * There are three "+1" votes from members of the core team. + + * There is no "-1" vote from any member of the core team. + + * The BDFLs haven't stepped in and executed their positive or negative + veto. + +When calling for a vote, the caller should specify a deadline by which +votes must be received. One week is generally suggested as the minimum +amount of time. + +Since this process allows any core committer to veto a proposal, any "-1" +votes (or BDFL vetos) should be accompanied by an explanation that explains +what it would take to convert that "-1" into at least a "+0". + +Whenever possible, these formal votes should be announced and held in +public on the `django-developers`_ mailing list. However, overly sensitive +or contentious issues -- including, most notably, votes on new core +committers -- may be held in private. Commit access ============= Django has two types of committers: -Full committers +Core committers These are people who have a long history of contributions to Django's - codebase, a solid track record of being polite and helpful on the mailing - lists, and a proven desire to dedicate serious time to Django's development. - - The bar is very high for full commit access. It will only be granted by - unanimous approval of all existing full committers, and the decision will err - on the side of rejection. + codebase, a solid track record of being polite and helpful on the + mailing lists, and a proven desire to dedicate serious time to Django's + development. The bar is high for full commit access. Partial committers These are people who are "domain experts." They have direct check-in access @@ -1206,10 +1323,12 @@ Partial committers is likely to be given to someone who contributes a large subframework to Django and wants to continue to maintain it. - Like full committers, partial commit access is by unanimous approval of all - full committers (and any other partial committers in the same area). - However, the bar is set lower; proven expertise in the area in question is - likely to be sufficient. + Partial commit access is granted by the same process as full + committers. However, the bar is set lower; proven expertise in the area + in question is likely to be sufficient. + +Decisions on new committers will follow the process explained above in `how +we make decisions`_. To request commit access, please contact an existing committer privately. Public requests for commit access are potential flame-war starters, and will be ignored. @@ -1224,3 +1343,4 @@ requests for commit access are potential flame-war starters, and will be ignored .. _pep8.py: http://pypi.python.org/pypi/pep8/ .. _i18n branch: http://code.djangoproject.com/browser/django/branches/i18n .. _`tags/releases`: http://code.djangoproject.com/browser/django/tags/releases +.. _`easy-pickings`: http://code.djangoproject.com/query?status=new&status=assigned&status=reopened&keywords=~easy-pickings&order=priority diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 8479a32bcfa9..e04579533803 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -1,5 +1,3 @@ -.. _internals-deprecation: - =========================== Django Deprecation Timeline =========================== @@ -52,7 +50,7 @@ their deprecation, as per the :ref:`Django deprecation policy associated methods (``user.message_set.create()`` and ``user.get_and_delete_messages()``), which have been deprecated since the 1.2 release, will be removed. The - :ref:`messages framework ` should be used + :doc:`messages framework ` should be used instead. * Authentication backends need to support the ``obj`` parameter for diff --git a/docs/internals/documentation.txt b/docs/internals/documentation.txt index 81480abf9ab6..36270eafb7e4 100644 --- a/docs/internals/documentation.txt +++ b/docs/internals/documentation.txt @@ -1,5 +1,3 @@ -.. _internals-documentation: - How the Django documentation works ================================== @@ -15,9 +13,14 @@ __ http://docutils.sourceforge.net/ To actually build the documentation locally, you'll currently need to install Sphinx -- ``easy_install Sphinx`` should do the trick. -Then, building the html is easy; just ``make html`` from the ``docs`` directory. +.. note:: + + The Django documentation can be generated with Sphinx version 0.6 or + newer, but we recommend using Sphinx 1.0.2 or newer. + +Then, building the HTML is easy; just ``make html`` from the ``docs`` directory. -To get started contributing, you'll want to read the `ReStructuredText +To get started contributing, you'll want to read the `reStructuredText Primer`__. After that, you'll want to read about the `Sphinx-specific markup`__ that's used to manage metadata, indexing, and cross-references. @@ -83,27 +86,55 @@ __ http://sphinx.pocoo.org/markup/desc.html An example ---------- -For a quick example of how it all fits together, check this out: +For a quick example of how it all fits together, consider this hypothetical +example: - * First, the ``ref/settings.txt`` document starts out like this:: + * First, the ``ref/settings.txt`` document could have an overall layout + like this: - .. _ref-settings: + .. code-block:: rst - Available settings - ================== + ======== + Settings + ======== ... - * Next, if you look at the ``topics/settings.txt`` document, you can see how - a link to ``ref/settings`` works:: + .. _available-settings: Available settings ================== - For a full list of available settings, see the :ref:`settings reference - `. + ... + + .. _deprecated-settings: + + Deprecated settings + =================== + + ... + + * Next, the ``topics/settings.txt`` document could contain something like + this: + + .. code-block:: rst + + You can access a :ref:`listing of all available settings + `. For a list of deprecated settings see + :ref:`deprecated-settings`. + + You can find both in the :doc:`settings reference document `. + + We use the Sphinx doc_ cross reference element when we want to link to + another document as a whole and the ref_ element when we want to link to + an arbitrary location in a document. + +.. _doc: http://sphinx.pocoo.org/markup/inline.html#role-doc +.. _ref: http://sphinx.pocoo.org/markup/inline.html#role-ref + + * Next, notice how the settings are annotated: - * Next, notice how the settings (right now just the top few) are annotated:: + .. code-block:: rst .. setting:: ADMIN_FOR diff --git a/docs/internals/index.txt b/docs/internals/index.txt index 4f9007705e78..26c941a09644 100644 --- a/docs/internals/index.txt +++ b/docs/internals/index.txt @@ -1,5 +1,3 @@ -.. _internals-index: - Django internals ================ diff --git a/docs/internals/release-process.txt b/docs/internals/release-process.txt index 20bc36584411..2a56f0be9238 100644 --- a/docs/internals/release-process.txt +++ b/docs/internals/release-process.txt @@ -1,5 +1,3 @@ -.. _internals-release-process: - ======================== Django's release process ======================== diff --git a/docs/internals/svn.txt b/docs/internals/svn.txt index 372fbd120201..9efbe28913e1 100644 --- a/docs/internals/svn.txt +++ b/docs/internals/svn.txt @@ -1,5 +1,3 @@ -.. _internals-svn: - ================================= The Django source code repository ================================= @@ -24,7 +22,7 @@ The Django source code repository uses `Subversion`_ to track changes to the code over time, so you'll need a copy of the Subversion client (a program called ``svn``) on your computer, and you'll want to familiarize yourself with the basics of how Subversion -works. Subversion's web site offers downloads for various operating +works. Subversion's Web site offers downloads for various operating systems, and `a free online book`_ is available to help you get up to speed with using Subversion. @@ -36,7 +34,7 @@ repository address instead. At the top level of the repository are two directories: ``django`` contains the full source code for all Django releases, while ``djangoproject.com`` contains the source code and templates for the `djangoproject.com `_ -web site. For trying out in-development Django code, or contributing +Web site. For trying out in-development Django code, or contributing to Django, you'll always want to check out code from some location in the ``django`` directory. @@ -60,7 +58,7 @@ into three areas: .. _Subversion: http://subversion.tigris.org/ .. _a free online book: http://svnbook.red-bean.com/ -.. _A friendly web-based interface for browsing the code: http://code.djangoproject.com/browser/ +.. _A friendly Web-based interface for browsing the code: http://code.djangoproject.com/browser/ Working with Django's trunk @@ -87,8 +85,8 @@ the ``django`` module within your checkout. If you're going to be working on Django's code (say, to fix a bug or develop a new feature), you can probably stop reading here and move -over to :ref:`the documentation for contributing to Django -`, which covers things like the preferred +over to :doc:`the documentation for contributing to Django +`, which covers things like the preferred coding style and how to generate and submit a patch. @@ -129,20 +127,20 @@ part of Django itself, and so are no longer separately maintained: object-relational mapper. This has been part of Django since the 1.0 release, as the bundled application ``django.contrib.gis``. -* ``i18n``: Added :ref:`internationalization support ` to +* ``i18n``: Added :doc:`internationalization support ` to Django. This has been part of Django since the 0.90 release. * ``magic-removal``: A major refactoring of both the internals and public APIs of Django's object-relational mapper. This has been part of Django since the 0.95 release. -* ``multi-auth``: A refactoring of :ref:`Django's bundled - authentication framework ` which added support for +* ``multi-auth``: A refactoring of :doc:`Django's bundled + authentication framework ` which added support for :ref:`authentication backends `. This has been part of Django since the 0.95 release. -* ``new-admin``: A refactoring of :ref:`Django's bundled - administrative application `. This became part of +* ``new-admin``: A refactoring of :doc:`Django's bundled + administrative application `. This became part of Django as of the 0.91 release, but was superseded by another refactoring (see next listing) prior to the Django 1.0 release. diff --git a/docs/intro/index.txt b/docs/intro/index.txt index 2135bc7fe9be..bc61be778a25 100644 --- a/docs/intro/index.txt +++ b/docs/intro/index.txt @@ -1,9 +1,7 @@ -.. _intro-index: - Getting started =============== -New to Django? Or to web development in general? Well, you came to the right +New to Django? Or to Web development in general? Well, you came to the right place: read this material to quickly get up and running. .. toctree:: diff --git a/docs/intro/install.txt b/docs/intro/install.txt index 901bde01c2e1..327686fd2115 100644 --- a/docs/intro/install.txt +++ b/docs/intro/install.txt @@ -1,10 +1,8 @@ -.. _intro-install: - Quick install guide =================== Before you can use Django, you'll need to get it installed. We have a -:ref:`complete installation guide ` that covers all the +:doc:`complete installation guide ` that covers all the possibilities; this guide will guide you to a simple, minimal installation that'll work while you walk through the introduction. @@ -12,9 +10,9 @@ Install Python -------------- Being a Python Web framework, Django requires Python. It works with any Python -version from 2.4 to 2.6 (due to backwards +version from 2.4 to 2.7 (due to backwards incompatibilities in Python 3.0, Django does not currently work with -Python 3.0; see :ref:`the Django FAQ ` for more +Python 3.0; see :doc:`the Django FAQ ` for more information on supported Python versions and the 3.0 transition), but we recommend installing Python 2.5 or later. If you do so, you won't need to set up a database just yet: Python 2.5 or later includes a lightweight database called SQLite_. .. _sqlite: http://sqlite.org/ @@ -25,17 +23,17 @@ probably already have it installed. .. admonition:: Django on Jython If you use Jython_ (a Python implementation for the Java platform), you'll - need to follow a few additional steps. See :ref:`howto-jython` for details. + need to follow a few additional steps. See :doc:`/howto/jython` for details. .. _jython: http://www.jython.org/ You can verify that Python's installed by typing ``python`` from your shell; you should see something like:: - Python 2.5.1 (r251:54863, Jan 17 2008, 19:35:17) + Python 2.5.1 (r251:54863, Jan 17 2008, 19:35:17) [GCC 4.0.1 (Apple Inc. build 5465)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> - + Set up a database ----------------- @@ -57,19 +55,20 @@ Install Django You've got three easy options to install Django: - * Install a version of Django :ref:`provided by your operating system - distribution `. This is the quickest option for those + * Install a version of Django :doc:`provided by your operating system + distribution `. This is the quickest option for those who have operating systems that distribute Django. * :ref:`Install an official release `. This is the best approach for users who want a stable version number and aren't concerned about running a slightly older version of Django. - + * :ref:`Install the latest development version `. This is best for users who want the latest-and-greatest features and aren't afraid of running brand-new code. - -.. warning:: + +.. admonition:: Always refer to the documentation that corresponds to the + version of Django you're using! If you do either of the first two steps, keep an eye out for parts of the documentation marked **new in development version**. That phrase flags @@ -79,7 +78,7 @@ You've got three easy options to install Django: That's it! ---------- -That's it -- you can now :ref:`move onto the tutorial `. +That's it -- you can now :doc:`move onto the tutorial `. diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 594c9fe5826d..34572a6c806e 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -1,5 +1,3 @@ -.. _intro-overview: - ================== Django at a glance ================== @@ -11,8 +9,8 @@ overview of how to write a database-driven Web app with Django. The goal of this document is to give you enough technical specifics to understand how Django works, but this isn't intended to be a tutorial or reference -- but we've got both! When you're ready to start a project, you can -:ref:`start with the tutorial ` or :ref:`dive right into more -detailed documentation `. +:doc:`start with the tutorial ` or :doc:`dive right into more +detailed documentation `. Design your model ================= @@ -21,9 +19,10 @@ Although you can use Django without a database, it comes with an object-relational mapper in which you describe your database layout in Python code. -The :ref:`data-model syntax ` offers many rich ways of +The :doc:`data-model syntax ` 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:: +database-schema problems. Here's a quick example, which might be saved in +the file ``mysite/news/models.py``:: class Reporter(models.Model): full_name = models.CharField(max_length=70) @@ -56,10 +55,11 @@ tables in your database for whichever tables don't already exist. Enjoy the free API ================== -With that, you've got a free, and rich, :ref:`Python API ` to +With that, you've got a free, and rich, :doc:`Python API ` to access your data. The API is created on the fly, no code generation necessary:: - >>> from mysite.models import Reporter, Article + # Import the models we created from our "news" app + >>> from news.models import Reporter, Article # No reporters are in the system yet. >>> Reporter.objects.all() @@ -131,7 +131,7 @@ 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 :ref:`administrative interface ` -- a Web +production ready :doc:`administrative interface ` -- a Web site that lets authenticated users add, change and delete objects. It's as easy as registering your model in the admin site:: @@ -168,8 +168,8 @@ A clean, elegant URL scheme is an important detail in a high-quality Web 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 :ref:`URLconf -`. A table of contents for your app, it contains a simple mapping +To design URLs for an app, you create a Python module called a :doc:`URLconf +`. 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. @@ -179,9 +179,9 @@ example above:: from django.conf.urls.defaults import * urlpatterns = patterns('', - (r'^articles/(\d{4})/$', 'mysite.views.year_archive'), - (r'^articles/(\d{4})/(\d{2})/$', 'mysite.views.month_archive'), - (r'^articles/(\d{4})/(\d{2})/(\d+)/$', 'mysite.views.article_detail'), + (r'^articles/(\d{4})/$', 'news.views.year_archive'), + (r'^articles/(\d{4})/(\d{2})/$', 'news.views.month_archive'), + (r'^articles/(\d{4})/(\d{2})/(\d+)/$', 'news.views.article_detail'), ) The code above maps URLs, as simple regular expressions, to the location of @@ -197,7 +197,7 @@ is a simple Python function. Each view gets passed a request object -- which contains request metadata -- and the values captured in the regex. For example, if a user requested the URL "/articles/2005/05/39323/", Django -would call the function ``mysite.views.article_detail(request, +would call the function ``news.views.article_detail(request, '2005', '05', '39323')``. Write your views @@ -216,7 +216,7 @@ and renders the template with the retrieved data. Here's an example view for a_list = Article.objects.filter(pub_date__year=year) return render_to_response('news/year_archive.html', {'year': year, 'article_list': a_list}) -This example uses Django's :ref:`template system `, which has +This example uses Django's :doc:`template system `, which has several powerful features but strives to stay simple enough for non-programmers to use. @@ -307,17 +307,17 @@ This is just the surface This has been only a quick overview of Django's functionality. Some more useful features: - * A :ref:`caching framework ` that integrates with memcached + * A :doc:`caching framework ` that integrates with memcached or other backends. - * A :ref:`syndication framework ` that makes + * A :doc:`syndication framework ` that makes creating RSS and Atom feeds as easy as writing a small Python class. * More sexy automatically-generated admin features -- this overview barely scratched the surface. -The next obvious steps are for you to `download Django`_, read :ref:`the -tutorial ` and join `the community`_. Thanks for your +The next obvious steps are for you to `download Django`_, read :doc:`the +tutorial ` and join `the community`_. Thanks for your interest! .. _download Django: http://www.djangoproject.com/download/ diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index c38fa7d7f514..b21dcd16c159 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -1,5 +1,3 @@ -.. _intro-tutorial01: - ===================================== Writing your first Django app, part 1 ===================================== @@ -14,7 +12,7 @@ It'll consist of two parts: * A public site that lets people view polls and vote in them. * An admin site that lets you add, change and delete polls. -We'll assume you have :ref:`Django installed ` already. You can +We'll assume you have :doc:`Django installed ` already. You can tell Django is installed by running the Python interactive interpreter and typing ``import django``. If that command runs successfully, with no errors, Django is installed. @@ -41,14 +39,21 @@ From the command line, ``cd`` into a directory where you'd like to store your code, then run the command ``django-admin.py startproject mysite``. This will create a ``mysite`` directory in your current directory. +.. admonition:: Script name may differ in distribution packages + + If you installed Django using a Linux distribution's package manager + (e.g. apt-get or yum) ``django-admin.py`` may have been renamed to + ``django-admin``. You may continue through this documentation by omitting + ``.py`` from each command. + .. admonition:: Mac OS X permissions If you're using Mac OS X, you may see the message "permission denied" when you try to run ``django-admin.py startproject``. This is because, on Unix-based systems like OS X, a file must be marked as "executable" before it can be run as a program. To do this, open Terminal.app and navigate (using - the ``cd`` command) to the directory where :ref:`django-admin.py - ` is installed, then run the command + the ``cd`` command) to the directory where :doc:`django-admin.py + ` is installed, then run the command ``chmod +x django-admin.py``. .. note:: @@ -58,11 +63,11 @@ create a ``mysite`` directory in your current directory. ``django`` (which will conflict with Django itself) or ``test`` (which conflicts with a built-in Python package). -:ref:`django-admin.py ` should be on your system path if you +:doc:`django-admin.py ` should be on your system path if you installed Django via ``python setup.py``. If it's not on your path, you can find it in ``site-packages/django/bin``, where ```site-packages``` is a directory -within your Python installation. Consider symlinking to :ref:`django-admin.py -` from some place on your path, such as +within your Python installation. Consider symlinking to :doc:`django-admin.py +` from some place on your path, such as :file:`/usr/local/bin`. .. admonition:: Where should this code live? @@ -93,14 +98,14 @@ These files are: * :file:`manage.py`: A command-line utility that lets you interact with this Django project in various ways. You can read all the details about - :file:`manage.py` in :ref:`ref-django-admin`. + :file:`manage.py` in :doc:`/ref/django-admin`. * :file:`settings.py`: Settings/configuration for this Django project. - :ref:`topics-settings` will tell you all about how settings work. + :doc:`/topics/settings` will tell you all about how settings work. * :file:`urls.py`: The URL declarations for this Django project; a "table of contents" of your Django-powered site. You can read more about URLs in - :ref:`topics-http-urls`. + :doc:`/topics/http/urls`. .. _more about packages: http://docs.python.org/tutorial/modules.html#packages @@ -220,6 +225,8 @@ come with Django: * :mod:`django.contrib.sites` -- A framework for managing multiple sites with one Django installation. + * :mod:`django.contrib.messages` -- A messaging framework. + These applications are included by default as a convenience for the common case. Each of these applications makes use of at least one database table, though, @@ -265,15 +272,13 @@ so you can focus on writing code rather than creating directories. .. admonition:: Projects vs. apps What's the difference between a project and an app? An app is a Web - application that does something -- e.g., a weblog system, a database of + application that does something -- e.g., a Weblog system, a database of public records or a simple poll app. A project is a collection of configuration and apps for a particular Web site. A project can contain multiple apps. An app can be in multiple projects. -In this tutorial, we'll create our poll app in the :file:`mysite` directory, -for simplicity. As a consequence, the app will be coupled to the project -- -that is, Python code within the poll app will refer to ``mysite.polls``. -Later in this tutorial, we'll discuss decoupling your apps for distribution. +Your apps can live anywhere on your `Python path`_. In this tutorial, we'll +create our poll app in the :file:`mysite` directory for simplicity. To create your app, make sure you're in the :file:`mysite` directory and type this command: @@ -371,7 +376,7 @@ But first we need to tell our project that the ``polls`` app is installed. Django installation. Edit the :file:`settings.py` file again, and change the -:setting:`INSTALLED_APPS` setting to include the string ``'mysite.polls'``. So +:setting:`INSTALLED_APPS` setting to include the string ``'polls'``. So it'll look like this:: INSTALLED_APPS = ( @@ -379,10 +384,10 @@ it'll look like this:: 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', - 'mysite.polls' + 'polls' ) -Now Django knows ``mysite`` includes the ``polls`` app. Let's run another +Now Django knows to include the ``polls`` app. Let's run another command: .. code-block:: bash @@ -473,7 +478,7 @@ added to your project since the last time you ran syncdb. :djadmin:`syncdb` can be called as often as you like, and it will only ever create the tables that don't exist. -Read the :ref:`django-admin.py documentation ` for full +Read the :doc:`django-admin.py documentation ` for full information on what the ``manage.py`` utility can do. Playing with the API @@ -490,9 +495,9 @@ We're using this instead of simply typing "python", because ``manage.py`` sets up the project's environment for you. "Setting up the environment" involves two things: - * Putting ``mysite`` on ``sys.path``. For flexibility, several pieces of + * Putting ``polls`` on ``sys.path``. For flexibility, several pieces of Django refer to projects in Python dotted-path notation (e.g. - ``'mysite.polls.models'``). In order for this to work, the ``mysite`` + ``'polls.models'``). In order for this to work, the ``polls`` package has to be on ``sys.path``. We've already seen one example of this: the :setting:`INSTALLED_APPS` @@ -504,16 +509,16 @@ things: .. admonition:: Bypassing manage.py If you'd rather not use ``manage.py``, no problem. Just make sure ``mysite`` - is at the root level on the Python path (i.e., ``import mysite`` works) and - set the ``DJANGO_SETTINGS_MODULE`` environment variable to - ``mysite.settings``. + and ``polls`` are at the root level on the Python path (i.e., ``import mysite`` + and ``import polls`` work) and set the ``DJANGO_SETTINGS_MODULE`` environment + variable to ``mysite.settings``. - For more information on all of this, see the :ref:`django-admin.py - documentation `. + For more information on all of this, see the :doc:`django-admin.py + documentation `. -Once you're in the shell, explore the :ref:`database API `:: +Once you're in the shell, explore the :doc:`database API `:: - >>> from mysite.polls.models import Poll, Choice # Import the model classes we just wrote. + >>> from polls.models import Poll, Choice # Import the model classes we just wrote. # No polls are in the system yet. >>> Poll.objects.all() @@ -564,22 +569,6 @@ of this object. Let's fix that by editing the polls model (in the def __unicode__(self): return self.choice -.. admonition:: If :meth:`~django.db.models.Model.__unicode__` doesn't seem to work - - If you add the :meth:`~django.db.models.Model.__unicode__` method to your - models and don't see any change in how they're represented, you're most - likely using an old version of Django. (This version of the tutorial is - written for the latest development version of Django.) If you're using a - Subversion checkout of Django's development version (see :ref:`the - installation docs ` for more information), you shouldn't have - any problems. - - If you want to stick with an older version of Django, you'll want to switch - to `the Django 0.96 tutorial`_, because this tutorial covers several features - that only exist in the Django development version. - -.. _the Django 0.96 tutorial: http://www.djangoproject.com/documentation/0.96/tutorial01/ - It's important to add :meth:`~django.db.models.Model.__unicode__` methods to your models, not only for your own sanity when dealing with the interactive prompt, but also because objects' representations are used throughout Django's @@ -621,7 +610,7 @@ Note the addition of ``import datetime`` to reference Python's standard Save these changes and start a new Python interactive shell by running ``python manage.py shell`` again:: - >>> from mysite.polls.models import Poll, Choice + >>> from polls.models import Poll, Choice # Make sure our __unicode__() addition worked. >>> Poll.objects.all() @@ -693,9 +682,12 @@ Save these changes and start a new Python interactive shell by running >>> c = p.choice_set.filter(choice__startswith='Just hacking') >>> c.delete() -For more information on model relations, see :ref:`Accessing related objects -`. For full details on the database API, see our -:ref:`Database API reference `. +For more information on model relations, see :doc:`Accessing related objects +`. For more on how to use double underscores to perform +field lookups via the API, see `Field lookups`__. For full details on the +database API, see our :doc:`Database API reference `. + +__ http://docs.djangoproject.com/en/1.2/topics/db/queries/#field-lookups -When you're comfortable with the API, read :ref:`part 2 of this tutorial -` to get Django's automatic admin working. +When you're comfortable with the API, read :doc:`part 2 of this tutorial +` to get Django's automatic admin working. diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index a7ab158faa88..c80d87d83578 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -1,10 +1,8 @@ -.. _intro-tutorial02: - ===================================== Writing your first Django app, part 2 ===================================== -This tutorial begins where :ref:`Tutorial 1 ` left off. We're +This tutorial begins where :doc:`Tutorial 1 ` left off. We're continuing the Web-poll application and will focus on Django's automatically-generated admin site. @@ -105,7 +103,7 @@ Just one thing to do: We need to tell the admin that ``Poll`` objects have an admin interface. To do this, create a file called ``admin.py`` in your ``polls`` directory, and edit it to look like this:: - from mysite.polls.models import Poll + from polls.models import Poll from django.contrib import admin admin.site.register(Poll) @@ -241,7 +239,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 mysite.polls.models import Choice + from polls.models import Choice admin.site.register(Choice) @@ -286,7 +284,7 @@ registration code to read:: This tells Django: "Choice objects are edited on the Poll admin page. By default, provide enough fields for 3 choices." -Load the "Add poll" page to see how that looks: +Load the "Add poll" page to see how that looks, you may need to restart your development server: .. image:: _images/admin11t.png :alt: Add poll page now has choices on it @@ -352,7 +350,7 @@ method (in ``models.py``) a ``short_description`` attribute:: return self.pub_date.date() == datetime.date.today() was_published_today.short_description = 'Published today?' -Let's add another improvement to the Poll change list page: Filters. Add the +Edit your admin.py file again and add an improvement to the Poll change list page: Filters. Add the following line to ``PollAdmin``:: list_filter = ['pub_date'] @@ -426,7 +424,7 @@ Then, just edit the file and replace the generic Django text with your own site's name as you see fit. This template file contains lots of text like ``{% block branding %}`` -and ``{{ title }}. The ``{%`` and ``{{`` tags are part of Django's +and ``{{ title }}``. The ``{%`` and ``{{`` tags are part of Django's template language. When Django renders ``admin/base_site.html``, this template language will be evaluated to produce the final HTML page. Don't worry if you can't make any sense of the template right now -- @@ -463,5 +461,5 @@ object-specific admin pages in whatever way you think is best. Again, don't worry if you can't understand the template language -- we'll cover that in more detail in Tutorial 3. -When you're comfortable with the admin site, read :ref:`part 3 of this tutorial -` to start working on public poll views. +When you're comfortable with the admin site, read :doc:`part 3 of this tutorial +` to start working on public poll views. diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index 0e09693778ef..88b4bea34f4d 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -1,10 +1,8 @@ -.. _intro-tutorial03: - ===================================== Writing your first Django app, part 3 ===================================== -This tutorial begins where :ref:`Tutorial 2 ` left off. We're +This tutorial begins where :doc:`Tutorial 2 ` left off. We're continuing the Web-poll application and will focus on creating the public interface -- "views." @@ -12,7 +10,7 @@ Philosophy ========== A view is a "type" of Web page in your Django application that generally serves -a specific function and has a specific template. For example, in a weblog +a specific function and has a specific template. For example, in a Weblog application, you might have the following views: * Blog homepage -- displays the latest few entries. @@ -31,7 +29,7 @@ application, you might have the following views: In our poll application, we'll have the following four views: - * Poll "archive" page -- displays the latest few polls. + * Poll "index" page -- displays the latest few polls. * Poll "detail" page -- displays a poll question, with no results but with a form to vote. @@ -68,8 +66,8 @@ arbitrary keyword arguments from the dictionary (an optional third item in the tuple). For more on :class:`~django.http.HttpRequest` objects, see the -:ref:`ref-request-response`. For more details on URLconfs, see the -:ref:`topics-http-urls`. +:doc:`/ref/request-response`. For more details on URLconfs, see the +:doc:`/topics/http/urls`. When you ran ``django-admin.py startproject mysite`` at the beginning of Tutorial 1, it created a default URLconf in ``mysite/urls.py``. It also @@ -86,10 +84,10 @@ Time for an example. Edit ``mysite/urls.py`` so it looks like this:: admin.autodiscover() urlpatterns = patterns('', - (r'^polls/$', 'mysite.polls.views.index'), - (r'^polls/(?P\d+)/$', 'mysite.polls.views.detail'), - (r'^polls/(?P\d+)/results/$', 'mysite.polls.views.results'), - (r'^polls/(?P\d+)/vote/$', 'mysite.polls.views.vote'), + (r'^polls/$', 'polls.views.index'), + (r'^polls/(?P\d+)/$', 'polls.views.detail'), + (r'^polls/(?P\d+)/results/$', 'polls.views.results'), + (r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), (r'^admin/', include(admin.site.urls)), ) @@ -98,8 +96,8 @@ This is worth a review. When somebody requests a page from your Web site -- say, the :setting:`ROOT_URLCONF` setting. It finds the variable named ``urlpatterns`` and traverses the regular expressions in order. When it finds a regular expression that matches -- ``r'^polls/(?P\d+)/$'`` -- it loads the -function ``detail()`` from ``mysite/polls/views.py``. Finally, -it calls that ``detail()`` function like so:: +function ``detail()`` from ``polls/views.py``. Finally, it calls that +``detail()`` function like so:: detail(request=, poll_id='23') @@ -114,7 +112,7 @@ what you can do with them. And there's no need to add URL cruft such as ``.php`` -- unless you have a sick sense of humor, in which case you can do something like this:: - (r'^polls/latest\.php$', 'mysite.polls.views.index'), + (r'^polls/latest\.php$', 'polls.views.index'), But, don't do that. It's silly. @@ -150,17 +148,17 @@ You should get a pleasantly-colored error page with the following message:: ViewDoesNotExist at /polls/ - Tried index in module mysite.polls.views. Error was: 'module' + Tried index in module polls.views. Error was: 'module' object has no attribute 'index' This error happened because you haven't written a function ``index()`` in the -module ``mysite/polls/views.py``. +module ``polls/views.py``. Try "/polls/23/", "/polls/23/results/" and "/polls/23/vote/". The error messages tell you which view Django tried (and failed to find, because you haven't written any views yet). -Time to write the first view. Open the file ``mysite/polls/views.py`` +Time to write the first view. Open the file ``polls/views.py`` and put the following Python code in it:: from django.http import HttpResponse @@ -205,11 +203,11 @@ you want, using whatever Python libraries you want. All Django wants is that :class:`~django.http.HttpResponse`. Or an exception. Because it's convenient, let's use Django's own database API, which we covered -in :ref:`Tutorial 1 `. Here's one stab at the ``index()`` +in :doc:`Tutorial 1 `. Here's one stab at the ``index()`` view, which displays the latest 5 poll questions in the system, separated by commas, according to publication date:: - from mysite.polls.models import Poll + from polls.models import Poll from django.http import HttpResponse def index(request): @@ -222,7 +220,7 @@ you want to change the way the page looks, you'll have to edit this Python code. So let's use Django's template system to separate the design from Python:: from django.template import Context, loader - from mysite.polls.models import Poll + from polls.models import Poll from django.http import HttpResponse def index(request): @@ -281,7 +279,7 @@ template. Django provides a shortcut. Here's the full ``index()`` view, rewritten:: from django.shortcuts import render_to_response - from mysite.polls.models import Poll + from polls.models import Poll def index(request): latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5] @@ -425,7 +423,7 @@ Method-calling happens in the ``{% for %}`` loop: ``poll.choice_set.all`` is interpreted as the Python code ``poll.choice_set.all()``, which returns an iterable of Choice objects and is suitable for use in the ``{% for %}`` tag. -See the :ref:`template guide ` for more about templates. +See the :doc:`template guide ` for more about templates. Simplifying the URLconfs ======================== @@ -434,19 +432,19 @@ Take some time to play around with the views and template system. As you edit the URLconf, you may notice there's a fair bit of redundancy in it:: urlpatterns = patterns('', - (r'^polls/$', 'mysite.polls.views.index'), - (r'^polls/(?P\d+)/$', 'mysite.polls.views.detail'), - (r'^polls/(?P\d+)/results/$', 'mysite.polls.views.results'), - (r'^polls/(?P\d+)/vote/$', 'mysite.polls.views.vote'), + (r'^polls/$', 'polls.views.index'), + (r'^polls/(?P\d+)/$', 'polls.views.detail'), + (r'^polls/(?P\d+)/results/$', 'polls.views.results'), + (r'^polls/(?P\d+)/vote/$', 'polls.views.vote'), ) -Namely, ``mysite.polls.views`` is in every callback. +Namely, ``polls.views`` is in every callback. Because this is a common case, the URLconf framework provides a shortcut for common prefixes. You can factor out the common prefixes and add them as the first argument to :func:`~django.conf.urls.defaults.patterns`, like so:: - urlpatterns = patterns('mysite.polls.views', + urlpatterns = patterns('polls.views', (r'^polls/$', 'index'), (r'^polls/(?P\d+)/$', 'detail'), (r'^polls/(?P\d+)/results/$', 'results'), @@ -456,6 +454,27 @@ first argument to :func:`~django.conf.urls.defaults.patterns`, like so:: This is functionally identical to the previous formatting. It's just a bit tidier. +Since you generally don't want the prefix for one app to be applied to every +callback in your URLconf, you can concatenate multiple +:func:`~django.conf.urls.defaults.patterns`. Your full ``mysite/urls.py`` might +now look like this:: + + from django.conf.urls.defaults import * + + from django.contrib import admin + admin.autodiscover() + + urlpatterns = patterns('polls.views', + (r'^polls/$', 'index'), + (r'^polls/(?P\d+)/$', 'detail'), + (r'^polls/(?P\d+)/results/$', 'results'), + (r'^polls/(?P\d+)/vote/$', 'vote'), + ) + + urlpatterns += patterns('', + (r'^admin/', include(admin.site.urls)), + ) + Decoupling the URLconfs ======================= @@ -472,16 +491,22 @@ We've been editing the URLs in ``mysite/urls.py``, but the URL design of an app is specific to the app, not to the Django installation -- so let's move the URLs within the app directory. -Copy the file ``mysite/urls.py`` to ``mysite/polls/urls.py``. Then, change +Copy the file ``mysite/urls.py`` to ``polls/urls.py``. Then, change ``mysite/urls.py`` to remove the poll-specific URLs and insert an -:func:`~django.conf.urls.defaults.include`:: +:func:`~django.conf.urls.defaults.include`, leaving you with:: - # ... + # This also imports the include function + from django.conf.urls.defaults import * + + from django.contrib import admin + admin.autodiscover() + urlpatterns = patterns('', - (r'^polls/', include('mysite.polls.urls')), - # ... + (r'^polls/', include('polls.urls')), + (r'^admin/', include(admin.site.urls)), + ) -:func:`~django.conf.urls.defaults.include`, simply, references another URLconf. +:func:`~django.conf.urls.defaults.include` simply references another URLconf. Note that the regular expression doesn't have a ``$`` (end-of-string match character) but has the trailing slash. Whenever Django encounters :func:`~django.conf.urls.defaults.include`, it chops off whatever part of the @@ -493,14 +518,17 @@ Here's what happens if a user goes to "/polls/34/" in this system: * Django will find the match at ``'^polls/'`` * Then, Django will strip off the matching text (``"polls/"``) and send the - remaining text -- ``"34/"`` -- to the 'mysite.polls.urls' URLconf for + remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for further processing. -Now that we've decoupled that, we need to decouple the 'mysite.polls.urls' +Now that we've decoupled that, we need to decouple the ``polls.urls`` URLconf by removing the leading "polls/" from each line, and removing the -lines registering the admin site:: +lines registering the admin site. Your ``polls.urls`` file should now look like +this:: + + from django.conf.urls.defaults import * - urlpatterns = patterns('mysite.polls.views', + urlpatterns = patterns('polls.views', (r'^$', 'index'), (r'^(?P\d+)/$', 'detail'), (r'^(?P\d+)/results/$', 'results'), @@ -510,9 +538,9 @@ lines registering the admin site:: The idea behind :func:`~django.conf.urls.defaults.include` and URLconf decoupling is to make it easy to plug-and-play URLs. Now that polls are in their own URLconf, they can be placed under "/polls/", or under "/fun_polls/", or -under "/content/polls/", or any other URL root, and the app will still work. +under "/content/polls/", or any other path root, and the app will still work. -All the poll app cares about is its relative URLs, not its absolute URLs. +All the poll app cares about is its relative path, not its absolute path. -When you're comfortable with writing views, read :ref:`part 4 of this tutorial -` to learn about simple form processing and generic views. +When you're comfortable with writing views, read :doc:`part 4 of this tutorial +` to learn about simple form processing and generic views. diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index ee3a3b204582..dfbd82df5548 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -1,10 +1,8 @@ -.. _intro-tutorial04: - ===================================== Writing your first Django app, part 4 ===================================== -This tutorial begins where :ref:`Tutorial 3 ` left off. We're +This tutorial begins where :doc:`Tutorial 3 ` left off. We're continuing the Web-poll application and will focus on simple form processing and cutting down our code. @@ -70,19 +68,19 @@ The details of how this works are explained in the documentation for :ref:`RequestContext `. Now, let's create a Django view that handles the submitted data and does -something with it. Remember, in :ref:`Tutorial 3 `, we +something with it. Remember, in :doc:`Tutorial 3 `, we created a URLconf for the polls application that includes this line:: (r'^(?P\d+)/vote/$', 'vote'), We also created a dummy implementation of the ``vote()`` function. Let's -create a real version. Add the following to ``mysite/polls/views.py``:: +create a real version. Add the following to ``polls/views.py``:: from django.shortcuts import get_object_or_404, render_to_response from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse from django.template import RequestContext - from mysite.polls.models import Choice, Poll + from polls.models import Choice, Poll # ... def vote(request, poll_id): p = get_object_or_404(Poll, pk=poll_id) @@ -100,7 +98,7 @@ create a real version. Add the following to ``mysite/polls/views.py``:: # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. - return HttpResponseRedirect(reverse('mysite.polls.views.results', args=(p.id,))) + return HttpResponseRedirect(reverse('polls.views.results', args=(p.id,))) This code includes a few things we haven't covered yet in this tutorial: @@ -149,7 +147,7 @@ This code includes a few things we haven't covered yet in this tutorial: As mentioned in Tutorial 3, ``request`` is a :class:`~django.http.HttpRequest` object. For more on :class:`~django.http.HttpRequest` objects, see the -:ref:`request and response documentation `. +:doc:`request and response documentation `. After somebody votes in a poll, the ``vote()`` view redirects to the results page for the poll. Let's write that view:: @@ -158,8 +156,8 @@ page for the poll. Let's write that view:: p = get_object_or_404(Poll, pk=poll_id) return render_to_response('polls/results.html', {'poll': p}) -This is almost exactly the same as the ``detail()`` view from :ref:`Tutorial 3 -`. The only difference is the template name. We'll fix this +This is almost exactly the same as the ``detail()`` view from :doc:`Tutorial 3 +`. The only difference is the template name. We'll fix this redundancy later. Now, create a ``results.html`` template: @@ -183,7 +181,7 @@ without having chosen a choice, you should see the error message. Use generic views: Less code is better ====================================== -The ``detail()`` (from :ref:`Tutorial 3 `) and ``results()`` +The ``detail()`` (from :doc:`Tutorial 3 `) and ``results()`` views are stupidly simple -- and, as mentioned above, redundant. The ``index()`` view (also from Tutorial 3), which displays a list of polls, is similar. @@ -224,7 +222,7 @@ tutorial so far:: from django.conf.urls.defaults import * - urlpatterns = patterns('mysite.polls.views', + urlpatterns = patterns('polls.views', (r'^$', 'index'), (r'^(?P\d+)/$', 'detail'), (r'^(?P\d+)/results/$', 'results'), @@ -234,7 +232,7 @@ tutorial so far:: Change it like so:: from django.conf.urls.defaults import * - from mysite.polls.models import Poll + from polls.models import Poll info_dict = { 'queryset': Poll.objects.all(), @@ -244,7 +242,7 @@ Change it like so:: (r'^$', 'django.views.generic.list_detail.object_list', info_dict), (r'^(?P\d+)/$', 'django.views.generic.list_detail.object_detail', info_dict), url(r'^(?P\d+)/results/$', 'django.views.generic.list_detail.object_detail', dict(info_dict, template_name='polls/results.html'), 'poll_results'), - (r'^(?P\d+)/vote/$', 'mysite.polls.views.vote'), + (r'^(?P\d+)/vote/$', 'polls.views.vote'), ) We're using two generic views here: @@ -328,8 +326,8 @@ are) used multiple times -- but we can use the name we've given:: Run the server, and use your new polling app based on generic views. -For full details on generic views, see the :ref:`generic views documentation -`. +For full details on generic views, see the :doc:`generic views documentation +`. Coming soon =========== @@ -344,5 +342,5 @@ will cover: * Advanced admin features: Permissions * Advanced admin features: Custom JavaScript -In the meantime, you might want to check out some pointers on :ref:`where to go -from here ` +In the meantime, you might want to check out some pointers on :doc:`where to go +from here ` diff --git a/docs/intro/whatsnext.txt b/docs/intro/whatsnext.txt index 0949b2299e94..00c1654c19e1 100644 --- a/docs/intro/whatsnext.txt +++ b/docs/intro/whatsnext.txt @@ -1,10 +1,8 @@ -.. _intro-whatsnext: - ================= What to read next ================= -So you've read all the :ref:`introductory material ` and have +So you've read all the :doc:`introductory material ` and have decided you'd like to keep using Django. We've only just scratched the surface with this intro (in fact, if you've read every single word you've still read less than 10% of the overall documentation). @@ -37,15 +35,15 @@ How the documentation is organized Django's main documentation is broken up into "chunks" designed to fill different needs: - * The :ref:`introductory material ` is designed for people new - to Django -- or to web development in general. It doesn't cover anything + * The :doc:`introductory material ` is designed for people new + to Django -- or to Web development in general. It doesn't cover anything in depth, but instead gives a high-level overview of how developing in Django "feels". - * The :ref:`topic guides `, on the other hand, dive deep into + * The :doc:`topic guides `, on the other hand, dive deep into individual parts of Django. There are complete guides to Django's - :ref:`model system `, :ref:`template engine - `, :ref:`forms framework `, and much + :doc:`model system `, :doc:`template engine + `, :doc:`forms framework `, and much more. This is probably where you'll want to spend most of your time; if you work @@ -53,27 +51,27 @@ different needs: everything there is to know about Django. * Web development is often broad, not deep -- problems span many domains. - We've written a set of :ref:`how-to guides ` that answer + We've written a set of :doc:`how-to guides ` that answer common "How do I ...?" questions. Here you'll find information about - :ref:`generating PDFs with Django `, :ref:`writing - custom template tags `, and more. + :doc:`generating PDFs with Django `, :doc:`writing + custom template tags `, and more. - Answers to really common questions can also be found in the :ref:`FAQ - `. + Answers to really common questions can also be found in the :doc:`FAQ + `. * The guides and how-to's don't cover every single class, function, and method available in Django -- that would be overwhelming when you're trying to learn. Instead, details about individual classes, functions, - methods, and modules are kept in the :ref:`reference `. This is + methods, and modules are kept in the :doc:`reference `. This is where you'll turn to find the details of a particular function or whathaveyou. * Finally, there's some "specialized" documentation not usually relevant to - most developers. This includes the :ref:`release notes `, - :ref:`documentation of obsolete features `, - :ref:`internals documentation ` for those who want to add - code to Django itself, and a :ref:`few other things that simply don't fit - elsewhere `. + most developers. This includes the :doc:`release notes `, + :doc:`documentation of obsolete features `, + :doc:`internals documentation ` for those who want to add + code to Django itself, and a :doc:`few other things that simply don't fit + elsewhere `. How documentation is updated @@ -168,7 +166,7 @@ You can get a local copy of the HTML documentation following a few easy steps: * Django's documentation uses a system called Sphinx__ to convert from plain text to HTML. You'll need to install Sphinx by either downloading - and installing the package from the Sphinx website, or by Python's + and installing the package from the Sphinx Web site, or by Python's ``easy_install``: .. code-block:: bash @@ -187,11 +185,10 @@ You can get a local copy of the HTML documentation following a few easy steps: * The HTML documentation will be placed in ``docs/_build/html``. -.. warning:: +.. note:: - At the time of this writing, Django's using a version of Sphinx not - yet released, so you'll currently need to install Sphinx from the - source. We'll fix this shortly. + Generation of the Django documentation will work with Sphinx version 0.6 + or newer, but we recommend going straight to Sphinx 1.0.2 or newer. __ http://sphinx.pocoo.org/ __ http://www.gnu.org/software/make/ diff --git a/docs/man/daily_cleanup.1 b/docs/man/daily_cleanup.1 index 444d4d0e6e52..dfcde1dff758 100644 --- a/docs/man/daily_cleanup.1 +++ b/docs/man/daily_cleanup.1 @@ -1,6 +1,6 @@ .TH "daily_cleanup.py" "1" "August 2007" "Django Project" "" .SH "NAME" -daily_cleanup.py \- Database clean-up for the Django web framework +daily_cleanup.py \- Database clean-up for the Django Web framework .SH "SYNOPSIS" .B daily_cleanup.py diff --git a/docs/man/django-admin.1 b/docs/man/django-admin.1 index ce3fdb16754b..016c80f78f43 100644 --- a/docs/man/django-admin.1 +++ b/docs/man/django-admin.1 @@ -1,6 +1,6 @@ .TH "django-admin.py" "1" "March 2008" "Django Project" "" .SH "NAME" -django\-admin.py \- Utility script for the Django web framework +django\-admin.py \- Utility script for the Django Web framework .SH "SYNOPSIS" .B django\-admin.py .I @@ -27,6 +27,9 @@ Compiles .po files to .mo files for use with builtin gettext support. .BI "createcachetable [" "tablename" "]" Creates the table needed to use the SQL cache backend .TP +.BI "createsuperuser [" "\-\-username=USERNAME" "] [" "\-\-email=EMAIL" "]" +Creates a superuser account (a user who has all permissions). +.TP .B dbshell Runs the command\-line client for the specified .BI database ENGINE. @@ -37,10 +40,21 @@ Displays differences between the current and Django's default settings. Settings that don't appear in the defaults are followed by "###". .TP +.BI "dumpdata [" "\-\-all" "] [" "\-\-format=FMT" "] [" "\-\-indent=NUM" "] [" "\-\-natural=NATURAL" "] [" "appname appname appname.Model ..." "]" +Outputs to standard output all data in the database associated with the named +application(s). +.TP +.BI flush +Returns the database to the state it was in immediately after syncdb was +executed. +.TP .B inspectdb Introspects the database tables in the database specified in settings.py and outputs a Django model module. .TP +.BI "loaddata [" "fixture fixture ..." "]" +Searches for and loads the contents of the named fixture into the database. +.TP .BI "install [" "appname ..." "]" Executes .B sqlall @@ -81,6 +95,13 @@ given model module name(s). .BI "sqlclear [" "appname ..." "]" Prints the DROP TABLE SQL statements for the given app name(s). .TP +.BI "sqlcustom [" "appname ..." "]" +Prints the custom SQL statements for the given app name(s). +.TP +.BI "sqlflush [" "appname ..." "]" +Prints the SQL statements that would be executed for the "flush" +command. +.TP .BI "sqlindexes [" "appname ..." "]" Prints the CREATE INDEX SQL statements for the given model module name(s). .TP @@ -107,7 +128,11 @@ in the current directory. Creates the database tables for all apps in INSTALLED_APPS whose tables haven't already been created. .TP -.BI "test [" "\-\-verbosity" "] [" "appname ..." "]" +.BI "test [" "\-\-verbosity" "] [" "\-\-failfast" "] [" "appname ..." "]" +Runs the test suite for the specified applications, or the entire project if +no apps are specified +.TP +.BI "testserver [" "\-\-addrport=ipaddr|port" "] [" "fixture fixture ..." "]" Runs the test suite for the specified applications, or the entire project if no apps are specified .TP @@ -145,6 +170,11 @@ Verbosity level: 0=minimal output, 1=normal output, 2=all output. .I \-\-adminmedia=ADMIN_MEDIA_PATH Specifies the directory from which to serve admin media when using the development server. .TP +.I \-\-traceback +By default, django-admin.py will show a simple error message whenever an +error occurs. If you specify this option, django-admin.py will +output a full stack trace whenever an exception is raised. +.TP .I \-l, \-\-locale=LOCALE The locale to process when using makemessages or compilemessages. .TP @@ -155,15 +185,15 @@ The domain of the message files (default: "django") when using makemessages. The file extension(s) to examine (default: ".html", separate multiple extensions with commas, or use -e multiple times). .TP -.I \-e, \-\-symlinks +.I \-s, \-\-symlinks Follows symlinks to directories when examining source code and templates for translation strings. .TP -.I \-e, \-\-ignore=PATTERN +.I \-i, \-\-ignore=PATTERN Ignore files or directories matching this glob-style pattern. Use multiple times to ignore more. .TP -.I \-e, \-\-no\-default\-ignore +.I \-\-no\-default\-ignore Don't ignore the common private glob-style patterns 'CVS', '.*' and '*~'. .TP .I \-a, \-\-all @@ -174,6 +204,10 @@ In the absence of the .BI \-\-settings option, this environment variable defines the settings module to be read. It should be in Python-import form, e.g. "myproject.settings". +.I \-\-database=DB +Used to specify the database on which a command will operate. If not +specified, this option will default to an alias of "default". +.TP .SH "SEE ALSO" Full descriptions of all these options, with examples, as well as documentation diff --git a/docs/man/gather_profile_stats.1 b/docs/man/gather_profile_stats.1 index 5ff13d8e69ec..fc56ee229134 100644 --- a/docs/man/gather_profile_stats.1 +++ b/docs/man/gather_profile_stats.1 @@ -1,6 +1,6 @@ .TH "gather_profile_stats.py" "1" "August 2007" "Django Project" "" .SH "NAME" -gather_profile_stats.py \- Performance analysis tool for the Django web +gather_profile_stats.py \- Performance analysis tool for the Django Web framework .SH "SYNOPSIS" .B python gather_profile_stats.py diff --git a/docs/misc/api-stability.txt b/docs/misc/api-stability.txt index a648c873cc67..456d84b45f59 100644 --- a/docs/misc/api-stability.txt +++ b/docs/misc/api-stability.txt @@ -1,10 +1,8 @@ -.. _misc-api-stability: - ============= API stability ============= -:ref:`The release of Django 1.0 ` comes with a promise of API +:doc:`The release of Django 1.0 ` comes with a promise of API stability and forwards-compatibility. In a nutshell, this means that code you develop against Django 1.0 will continue to work against 1.1 unchanged, and you should need to make only minor changes for any 1.X release. @@ -37,67 +35,67 @@ Stable APIs =========== In general, everything covered in the documentation -- with the exception of -anything in the :ref:`internals area ` is considered stable as +anything in the :doc:`internals area ` is considered stable as of 1.0. This includes these APIs: - - :ref:`Authorization ` + - :doc:`Authorization ` - - :ref:`Caching `. + - :doc:`Caching `. - - :ref:`Model definition, managers, querying and transactions - ` + - :doc:`Model definition, managers, querying and transactions + ` - - :ref:`Sending e-mail `. + - :doc:`Sending e-mail `. - - :ref:`File handling and storage ` + - :doc:`File handling and storage ` - - :ref:`Forms ` + - :doc:`Forms ` - - :ref:`HTTP request/response handling `, including file + - :doc:`HTTP request/response handling `, including file uploads, middleware, sessions, URL resolution, view, and shortcut APIs. - - :ref:`Generic views `. + - :doc:`Generic views `. - - :ref:`Internationalization `. + - :doc:`Internationalization `. - - :ref:`Pagination ` + - :doc:`Pagination ` - - :ref:`Serialization ` + - :doc:`Serialization ` - - :ref:`Signals ` + - :doc:`Signals ` - - :ref:`Templates `, including the language, Python-level - :ref:`template APIs `, and :ref:`custom template tags - and libraries `. We may add new template + - :doc:`Templates `, including the language, Python-level + :doc:`template APIs `, and :doc:`custom template tags + and libraries `. We may add new template tags in the future and the names may inadvertently clash with external template tags. Before adding any such tags, we'll ensure that Django raises an error if it tries to load tags with duplicate names. - - :ref:`Testing ` + - :doc:`Testing ` - - :ref:`django-admin utility `. + - :doc:`django-admin utility `. - - :ref:`Built-in middleware ` + - :doc:`Built-in middleware ` - - :ref:`Request/response objects `. + - :doc:`Request/response objects `. - - :ref:`Settings `. Note, though that while the :ref:`list of - built-in settings ` can be considered complete we may -- and + - :doc:`Settings `. Note, though that while the :doc:`list of + built-in settings ` can be considered complete we may -- and probably will -- add new settings in future versions. This is one of those places where "'stable' does not mean 'complete.'" - - :ref:`Built-in signals `. Like settings, we'll probably add + - :doc:`Built-in signals `. Like settings, we'll probably add new signals in the future, but the existing ones won't break. - - :ref:`Unicode handling `. + - :doc:`Unicode handling `. - - Everything covered by the :ref:`HOWTO guides `. + - Everything covered by the :doc:`HOWTO guides `. ``django.utils`` ---------------- Most of the modules in ``django.utils`` are designed for internal use. Only -the following parts of :ref:`django.utils ` can be considered stable: +the following parts of :doc:`django.utils ` can be considered stable: - ``django.utils.cache`` - ``django.utils.datastructures.SortedDict`` -- only this single class; the @@ -127,7 +125,7 @@ Contributed applications (``django.contrib``) While we'll make every effort to keep these APIs stable -- and have no plans to break any contrib apps -- this is an area that will have more flux between -releases. As the web evolves, Django must evolve with it. +releases. As the Web evolves, Django must evolve with it. However, any changes to contrib apps will come with an important guarantee: we'll make sure it's always possible to use an older version of a contrib app if diff --git a/docs/misc/design-philosophies.txt b/docs/misc/design-philosophies.txt index 43bb8096c9aa..631097ae2bc4 100644 --- a/docs/misc/design-philosophies.txt +++ b/docs/misc/design-philosophies.txt @@ -1,5 +1,3 @@ -.. _misc-design-philosophies: - =================== Design philosophies =================== diff --git a/docs/misc/distributions.txt b/docs/misc/distributions.txt index 6a0845801d48..d9281ad3dab4 100644 --- a/docs/misc/distributions.txt +++ b/docs/misc/distributions.txt @@ -1,5 +1,3 @@ -.. _misc-distributions: - =================================== Third-party distributions of Django =================================== diff --git a/docs/misc/index.txt b/docs/misc/index.txt index 534171b6eda1..b42baeb9fdb2 100644 --- a/docs/misc/index.txt +++ b/docs/misc/index.txt @@ -1,5 +1,3 @@ -.. _misc-index: - Meta-documentation and miscellany ================================= diff --git a/docs/obsolete/admin-css.txt b/docs/obsolete/admin-css.txt index 4f8fb663e2d0..f4cca549b444 100644 --- a/docs/obsolete/admin-css.txt +++ b/docs/obsolete/admin-css.txt @@ -1,5 +1,3 @@ -.. _obsolete-admin-css: - ====================================== Customizing the Django admin interface ====================================== diff --git a/docs/obsolete/index.txt b/docs/obsolete/index.txt index 09e0826b88bf..ddc86237cc0f 100644 --- a/docs/obsolete/index.txt +++ b/docs/obsolete/index.txt @@ -1,5 +1,3 @@ -.. _obsolete-index: - Deprecated/obsolete documentation ================================= diff --git a/docs/ref/authbackends.txt b/docs/ref/authbackends.txt index 0e98c21b21f2..a50b414c782c 100644 --- a/docs/ref/authbackends.txt +++ b/docs/ref/authbackends.txt @@ -1,5 +1,3 @@ -.. _ref-authentication-backends: - ======================= Authentication backends ======================= @@ -10,8 +8,8 @@ Authentication backends This document details the authentication backends that come with Django. For information on how to use them and how to write your own authentication backends, see the :ref:`Other authentication sources section -` of the :ref:`User authentication guide -`. +` of the :doc:`User authentication guide +`. Available authentication backends @@ -33,5 +31,5 @@ The following backends are available in :mod:`django.contrib.auth.backends`: Use this backend to take advantage of external-to-Django-handled authentication. It authenticates using usernames passed in :attr:`request.META['REMOTE_USER'] `. See - the :ref:`Authenticating against REMOTE_USER ` + the :doc:`Authenticating against REMOTE_USER ` documentation. diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index 62f944d9b6cf..0fab59e3f36a 100644 --- a/docs/ref/contrib/admin/actions.txt +++ b/docs/ref/contrib/admin/actions.txt @@ -1,5 +1,3 @@ -.. _ref-contrib-admin-actions: - ============= Admin actions ============= @@ -177,6 +175,8 @@ Defining actions as methods gives the action more straightforward, idiomatic access to the :class:`ModelAdmin` itself, allowing the action to call any of the methods provided by the admin. +.. _custom-admin-action: + For example, we can use ``self`` to flash a message to the user informing her that the action was successful:: @@ -208,7 +208,7 @@ objects. To provide an intermediary page, simply return an :class:`~django.http.HttpResponse` (or subclass) from your action. For example, you might write a simple export function that uses Django's -:ref:`serialization functions ` to dump some selected +:doc:`serialization functions ` to dump some selected objects as JSON:: from django.http import HttpResponse @@ -294,7 +294,7 @@ Disabling a site-wide action site-wide. If, however, you need to re-enable a globally-disabled action for one - particular model, simply list it explicitally in your ``ModelAdmin.actions`` + particular model, simply list it explicitly in your ``ModelAdmin.actions`` list:: # Globally disable delete selected diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt new file mode 100644 index 000000000000..6743921d1eb5 --- /dev/null +++ b/docs/ref/contrib/admin/admindocs.txt @@ -0,0 +1,161 @@ +======================================== +The Django admin documentation generator +======================================== + +.. module:: django.contrib.admindocs + :synopsis: Django's admin documentation generator. + +.. currentmodule:: django.contrib.admindocs + +Django's :mod:`~django.contrib.admindocs` app pulls documentation from the +docstrings of models, views, template tags, and template filters for any app in +:setting:`INSTALLED_APPS` and makes that documentation available from the +:mod:`Django admin `. + +In addition to providing offline documentation for all template tags and +template filters that ship with Django, you may utilize admindocs to quickly +document your own code. + +Overview +======== + +To activate the :mod:`~django.contrib.admindocs`, you will need to do +the following: + + * Add :mod:`django.contrib.admindocs` to your :setting:`INSTALLED_APPS`. + * Add ``(r'^admin/doc/', include('django.contrib.admindocs.urls'))`` to + your :data:`urlpatterns`. Make sure it's included *before* the + ``r'^admin/'`` entry, so that requests to ``/admin/doc/`` don't get + handled by the latter entry. + * 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` 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 +upper right of the page. + +Documentation helpers +===================== + +The following special markup can be used in your docstrings to easily create +hyperlinks to other components: + +================= ======================= +Django Component reStructuredText roles +================= ======================= +Models ``:model:`appname.ModelName``` +Views ``:view:`appname.view_name``` +Template tags ``:tag:`tagname``` +Template filters ``:filter:`filtername``` +Templates ``:template:`path/to/template.html``` +================= ======================= + +Model reference +=============== + +The **models** section of the ``admindocs`` page describes each model in the +system along with all the fields and methods available on it. Relationships to +other models appear as hyperlinks. Descriptions are pulled from ``help_text`` +attributes on fields or from docstrings on model methods. + +A model with useful documentation might look like this:: + + class BlogEntry(models.Model): + """ + Stores a single blog entry, related to :model:`blog.Blog` and + :model:`auth.User`. + + """ + slug = models.SlugField(help_text="A short label, generally used in URLs.") + author = models.ForeignKey(User) + blog = models.ForeignKey(Blog) + ... + + def publish(self): + """Makes the blog entry live on the site.""" + ... + +View reference +============== + +Each URL in your site has a separate entry in the ``admindocs`` page, and +clicking on a given URL will show you the corresponding view. Helpful things +you can document in your view function docstrings include: + + * A short description of what the view does. + * The **context**, or a list of variables available in the view's template. + * The name of the template or templates that are used for that view. + +For example:: + + from myapp.models import MyModel + + def my_view(request, slug): + """ + Display an individual :model:`myapp.MyModel`. + + **Context** + + ``RequestContext`` + + ``mymodel`` + An instance of :model:`myapp.MyModel`. + + **Template:** + + :template:`myapp/my_template.html` + + """ + return render_to_response('myapp/my_template.html', { + 'mymodel': MyModel.objects.get(slug=slug) + }, context_instance=RequestContext(request)) + + +Template tags and filters reference +=================================== + +The **tags** and **filters** ``admindocs`` sections describe all the tags and +filters that come with Django (in fact, the :ref:`built-in tag reference +` and :ref:`built-in filter reference +` documentation come directly from those +pages). Any tags or filters that you create or are added by a third-party app +will show up in these sections as well. + + +Template reference +================== + +While ``admindocs`` does not include a place to document templates by +themselves, if you use the ``:template:`path/to/template.html``` syntax in a +docstring the resulting page will verify the path of that template with +Django's :ref:`template loaders `. This can be a handy way to +check if the specified template exists and to show where on the filesystem that +template is stored. + + +Included Bookmarklets +===================== + +Several useful bookmarklets are available from the ``admindocs`` page: + + Documentation for this page + Jumps you from any page to the documentation for the view that generates + that page. + + Show object ID + Shows the content-type and unique ID for pages that represent a single + object. + + Edit this object + Jumps to the admin page for pages that represent a single object. + +Using these bookmarklets requires that you are either logged into the +:mod:`Django 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 ` are 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 f7aefa457d7d..18ea121d4023 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -1,5 +1,3 @@ -.. _ref-contrib-admin: - ===================== The Django admin site ===================== @@ -7,8 +5,6 @@ The Django admin site .. module:: django.contrib.admin :synopsis: Django's admin site. -.. currentmodule:: django.contrib.admin - One of the most powerful parts of Django is the automatic admin interface. It reads metadata in your model to provide a powerful and production-ready interface that content producers can immediately use to start adding content to @@ -53,6 +49,7 @@ Other topics :maxdepth: 1 actions + admindocs .. seealso:: @@ -64,30 +61,31 @@ Other topics .. class:: ModelAdmin -The ``ModelAdmin`` class is the representation of a model in the admin -interface. These are stored in a file named ``admin.py`` in your application. -Let's take a look at a very simple example of the ``ModelAdmin``:: + The ``ModelAdmin`` class is the representation of a model in the admin + interface. These are stored in a file named ``admin.py`` in your + application. Let's take a look at a very simple example of + the ``ModelAdmin``:: - from django.contrib import admin - from myproject.myapp.models import Author + from django.contrib import admin + from myproject.myapp.models import Author - class AuthorAdmin(admin.ModelAdmin): - pass - admin.site.register(Author, AuthorAdmin) + class AuthorAdmin(admin.ModelAdmin): + pass + admin.site.register(Author, AuthorAdmin) -.. admonition:: Do you need a ``ModelAdmin`` object at all? + .. admonition:: Do you need a ``ModelAdmin`` object at all? - In the preceding example, the ``ModelAdmin`` class doesn't define any - custom values (yet). As a result, the default admin interface will be - provided. If you are happy with the default admin interface, you don't - need to define a ``ModelAdmin`` object at all -- you can register the - model class without providing a ``ModelAdmin`` description. The - preceding example could be simplified to:: + In the preceding example, the ``ModelAdmin`` class doesn't define any + custom values (yet). As a result, the default admin interface will be + provided. If you are happy with the default admin interface, you don't + need to define a ``ModelAdmin`` object at all -- you can register the + model class without providing a ``ModelAdmin`` description. The + preceding example could be simplified to:: - from django.contrib import admin - from myproject.myapp.models import Author + from django.contrib import admin + from myproject.myapp.models import Author - admin.site.register(Author) + admin.site.register(Author) ``ModelAdmin`` Options ---------------------- @@ -101,615 +99,629 @@ subclass:: .. attribute:: ModelAdmin.date_hierarchy -Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField`` in -your model, and the change list page will include a date-based drilldown -navigation by that field. + Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField`` + in your model, and the change list page will include a date-based drilldown + navigation by that field. -Example:: + Example:: - date_hierarchy = 'pub_date' + date_hierarchy = 'pub_date' .. attribute:: ModelAdmin.form -By default a ``ModelForm`` is dynamically created for your model. It is used -to create the form presented on both the add/change pages. You can easily -provide your own ``ModelForm`` to override any default form behavior on the -add/change pages. + By default a ``ModelForm`` is dynamically created for your model. It is + used to create the form presented on both the add/change pages. You can + easily provide your own ``ModelForm`` to override any default form behavior + on the add/change pages. -For an example see the section `Adding custom validation to the admin`_. + For an example see the section `Adding custom validation to the admin`_. .. attribute:: ModelAdmin.fieldsets -Set ``fieldsets`` to control the layout of admin "add" and "change" pages. + Set ``fieldsets`` to control the layout of admin "add" and "change" pages. + + ``fieldsets`` is a list of two-tuples, in which each two-tuple represents a + ``
          `` on the admin form page. (A ``
          `` is a "section" of + the form.) + + The two-tuples are in the format ``(name, field_options)``, where ``name`` + is a string representing the title of the fieldset and ``field_options`` is + a dictionary of information about the fieldset, including a list of fields + to be displayed in it. + + A full example, taken from the ``django.contrib.flatpages.FlatPage`` + model:: + + class FlatPageAdmin(admin.ModelAdmin): + fieldsets = ( + (None, { + 'fields': ('url', 'title', 'content', 'sites') + }), + ('Advanced options', { + 'classes': ('collapse',), + 'fields': ('enable_comments', 'registration_required', 'template_name') + }), + ) -``fieldsets`` is a list of two-tuples, in which each two-tuple represents a -``
          `` on the admin form page. (A ``
          `` is a "section" of the -form.) + This results in an admin page that looks like: -The two-tuples are in the format ``(name, field_options)``, where ``name`` is a -string representing the title of the fieldset and ``field_options`` is a -dictionary of information about the fieldset, including a list of fields to be -displayed in it. + .. image:: _images/flatfiles_admin.png -A full example, taken from the ``django.contrib.flatpages.FlatPage`` model:: + If ``fieldsets`` isn't given, Django will default to displaying each field + that isn't an ``AutoField`` and has ``editable=True``, in a single + fieldset, in the same order as the fields are defined in the model. - class FlatPageAdmin(admin.ModelAdmin): - fieldsets = ( - (None, { - 'fields': ('url', 'title', 'content', 'sites') - }), - ('Advanced options', { - 'classes': ('collapse',), - 'fields': ('enable_comments', 'registration_required', 'template_name') - }), - ) + The ``field_options`` dictionary can have the following keys: -This results in an admin page that looks like: + * ``fields`` + A tuple of field names to display in this fieldset. This key is + required. - .. image:: _images/flatfiles_admin.png + Example:: -If ``fieldsets`` isn't given, Django will default to displaying each field -that isn't an ``AutoField`` and has ``editable=True``, in a single fieldset, -in the same order as the fields are defined in the model. + { + 'fields': ('first_name', 'last_name', 'address', 'city', 'state'), + } -The ``field_options`` dictionary can have the following keys: + To display multiple fields on the same line, wrap those fields in + their own tuple. In this example, the ``first_name`` and + ``last_name`` fields will display on the same line:: - * ``fields`` - A tuple of field names to display in this fieldset. This key is - required. + { + 'fields': (('first_name', 'last_name'), 'address', 'city', 'state'), + } - Example:: + .. versionadded:: 1.2 - { - 'fields': ('first_name', 'last_name', 'address', 'city', 'state'), - } + ``fields`` can contain values defined in + :attr:`~ModelAdmin.readonly_fields` to be displayed as read-only. - To display multiple fields on the same line, wrap those fields in - their own tuple. In this example, the ``first_name`` and ``last_name`` - fields will display on the same line:: + * ``classes`` + A list containing extra CSS classes to apply to the fieldset. - { - 'fields': (('first_name', 'last_name'), 'address', 'city', 'state'), - } - - .. versionadded:: 1.2 + Example:: - ``fields`` can contain values defined in - :attr:`ModelAdmin.readonly_fields` to be displayed as read-only. + { + 'classes': ['wide', 'extrapretty'], + } - * ``classes`` - A list containing extra CSS classes to apply to the fieldset. + Two useful classes defined by the default admin site stylesheet are + ``collapse`` and ``wide``. Fieldsets with the ``collapse`` style + will be initially collapsed in the admin and replaced with a small + "click to expand" link. Fieldsets with the ``wide`` style will be + given extra horizontal space. - Example:: + * ``description`` + A string of optional extra text to be displayed at the top of each + fieldset, under the heading of the fieldset. - { - 'classes': ['wide', 'extrapretty'], - } - - Two useful classes defined by the default admin site stylesheet are - ``collapse`` and ``wide``. Fieldsets with the ``collapse`` style will - be initially collapsed in the admin and replaced with a small - "click to expand" link. Fieldsets with the ``wide`` style will be - given extra horizontal space. - - * ``description`` - A string of optional extra text to be displayed at the top of each - fieldset, under the heading of the fieldset. - - Note that this value is *not* HTML-escaped when it's displayed in - the admin interface. This lets you include HTML if you so desire. - Alternatively you can use plain text and - ``django.utils.html.escape()`` to escape any HTML special - characters. + Note that this value is *not* HTML-escaped when it's displayed in + the admin interface. This lets you include HTML if you so desire. + Alternatively you can use plain text and + ``django.utils.html.escape()`` to escape any HTML special + characters. .. attribute:: ModelAdmin.fields -Use this option as an alternative to ``fieldsets`` if the layout does not -matter and if you want to only show a subset of the available fields in the -form. For example, you could define a simpler version of the admin form for -the ``django.contrib.flatpages.FlatPage`` model as follows:: + Use this option as an alternative to ``fieldsets`` if the layout does not + matter and if you want to only show a subset of the available fields in the + form. For example, you could define a simpler version of the admin form for + the ``django.contrib.flatpages.FlatPage`` model as follows:: - class FlatPageAdmin(admin.ModelAdmin): - fields = ('url', 'title', 'content') + class FlatPageAdmin(admin.ModelAdmin): + fields = ('url', 'title', 'content') -In the above example, only the fields 'url', 'title' and 'content' will be -displayed, sequentially, in the form. + In the above example, only the fields 'url', 'title' and 'content' will be + displayed, sequentially, in the form. -.. versionadded:: 1.2 + .. versionadded:: 1.2 -``fields`` can contain values defined in :attr:`ModelAdmin.readonly_fields` -to be displayed as read-only. + ``fields`` can contain values defined in :attr:`ModelAdmin.readonly_fields` + to be displayed as read-only. -.. admonition:: Note + .. admonition:: Note - This ``fields`` option should not be confused with the ``fields`` - dictionary key that is within the ``fieldsets`` option, as described in - the previous section. + This ``fields`` option should not be confused with the ``fields`` + dictionary key that is within the ``fieldsets`` option, as described in + the previous section. -.. attribute:: ModelAdmin.exclude + .. attribute:: ModelAdmin.exclude -This attribute, if given, should be a list of field names to exclude from the -form. + This attribute, if given, should be a list of field names to exclude from + the form. -For example, let's consider the following model:: + For example, let's consider the following model:: - class Author(models.Model): - name = models.CharField(max_length=100) - title = models.CharField(max_length=3) - birth_date = models.DateField(blank=True, null=True) + class Author(models.Model): + name = models.CharField(max_length=100) + title = models.CharField(max_length=3) + birth_date = models.DateField(blank=True, null=True) -If you want a form for the ``Author`` model that includes only the ``name`` -and ``title`` fields, you would specify ``fields`` or ``exclude`` like this:: + If you want a form for the ``Author`` model that includes only the ``name`` + and ``title`` fields, you would specify ``fields`` or ``exclude`` like + this:: - class AuthorAdmin(admin.ModelAdmin): - fields = ('name', 'title') + class AuthorAdmin(admin.ModelAdmin): + fields = ('name', 'title') - class AuthorAdmin(admin.ModelAdmin): - exclude = ('birth_date',) + class AuthorAdmin(admin.ModelAdmin): + exclude = ('birth_date',) -Since the Author model only has three fields, ``name``, ``title``, and -``birth_date``, the forms resulting from the above declarations will contain -exactly the same fields. + Since the Author model only has three fields, ``name``, ``title``, and + ``birth_date``, the forms resulting from the above declarations will + contain exactly the same fields. -.. attribute:: ModelAdmin.filter_horizontal + .. attribute:: ModelAdmin.filter_horizontal -Use a nifty unobtrusive JavaScript "filter" interface instead of the -usability-challenged ```` in the admin form. The value is + a list of fields that should be displayed as a horizontal filter interface. + See ``filter_vertical`` to use a vertical interface. -.. attribute:: ModelAdmin.filter_vertical + .. attribute:: ModelAdmin.filter_vertical -Same as ``filter_horizontal``, but is a vertical display of the filter -interface. + Same as ``filter_horizontal``, but is a vertical display of the filter + interface. .. attribute:: ModelAdmin.list_display -Set ``list_display`` to control which fields are displayed on the change list -page of the admin. + Set ``list_display`` to control which fields are displayed on the change + list page of the admin. -Example:: + Example:: - list_display = ('first_name', 'last_name') + list_display = ('first_name', 'last_name') -If you don't set ``list_display``, the admin site will display a single column -that displays the ``__unicode__()`` representation of each object. + If you don't set ``list_display``, the admin site will display a single + column that displays the ``__unicode__()`` representation of each object. -You have four possible values that can be used in ``list_display``: + You have four possible values that can be used in ``list_display``: - * A field of the model. For example:: + * A field of the model. For example:: - class PersonAdmin(admin.ModelAdmin): - list_display = ('first_name', 'last_name') + class PersonAdmin(admin.ModelAdmin): + list_display = ('first_name', 'last_name') - * A callable that accepts one parameter for the model instance. For - example:: + * A callable that accepts one parameter for the model instance. For + example:: - def upper_case_name(obj): - return ("%s %s" % (obj.first_name, obj.last_name)).upper() - upper_case_name.short_description = 'Name' + def upper_case_name(obj): + return ("%s %s" % (obj.first_name, obj.last_name)).upper() + upper_case_name.short_description = 'Name' - class PersonAdmin(admin.ModelAdmin): - list_display = (upper_case_name,) + class PersonAdmin(admin.ModelAdmin): + list_display = (upper_case_name,) - * A string representing an attribute on the ``ModelAdmin``. This behaves - same as the callable. For example:: + * A string representing an attribute on the ``ModelAdmin``. This + behaves same as the callable. For example:: - class PersonAdmin(admin.ModelAdmin): - list_display = ('upper_case_name',) + class PersonAdmin(admin.ModelAdmin): + list_display = ('upper_case_name',) - def upper_case_name(self, obj): - return ("%s %s" % (obj.first_name, obj.last_name)).upper() - upper_case_name.short_description = 'Name' + def upper_case_name(self, obj): + 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:: + * 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:: - class Person(models.Model): - name = models.CharField(max_length=50) - birthday = models.DateField() + class Person(models.Model): + name = models.CharField(max_length=50) + birthday = models.DateField() - def decade_born_in(self): - return self.birthday.strftime('%Y')[:3] + "0's" - decade_born_in.short_description = 'Birth decade' + def decade_born_in(self): + return self.birthday.strftime('%Y')[:3] + "0's" + decade_born_in.short_description = 'Birth decade' - class PersonAdmin(admin.ModelAdmin): - list_display = ('name', 'decade_born_in') + class PersonAdmin(admin.ModelAdmin): + list_display = ('name', 'decade_born_in') -A few special cases to note about ``list_display``: + A few special cases to note about ``list_display``: - * If the field is a ``ForeignKey``, Django will display the - ``__unicode__()`` of the related object. + * If the field is a ``ForeignKey``, Django will display the + ``__unicode__()`` of the related object. - * ``ManyToManyField`` fields aren't supported, because that would entail - executing a separate SQL statement for each row in the table. If you - want to do this nonetheless, give your model a custom method, and add - that method's name to ``list_display``. (See below for more on custom - methods in ``list_display``.) + * ``ManyToManyField`` fields aren't supported, because that would + entail executing a separate SQL statement for each row in the table. + If you want to do this nonetheless, give your model a custom method, + and add that method's name to ``list_display``. (See below for more + on custom methods in ``list_display``.) - * If the field is a ``BooleanField`` or ``NullBooleanField``, Django will - display a pretty "on" or "off" icon instead of ``True`` or ``False``. + * If the field is a ``BooleanField`` or ``NullBooleanField``, Django + will display a pretty "on" or "off" icon instead of ``True`` or + ``False``. - * If the string given is a method of the model, ``ModelAdmin`` or a - callable, Django will HTML-escape the output by default. If you'd rather - not escape the output of the method, give the method an ``allow_tags`` - attribute whose value is ``True``. + * If the string given is a method of the model, ``ModelAdmin`` or a + callable, Django will HTML-escape the output by default. If you'd + rather not escape the output of the method, give the method an + ``allow_tags`` attribute whose value is ``True``. - Here's a full example model:: + Here's a full example model:: - class Person(models.Model): - first_name = models.CharField(max_length=50) - last_name = models.CharField(max_length=50) - color_code = models.CharField(max_length=6) + class Person(models.Model): + first_name = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + color_code = models.CharField(max_length=6) - def colored_name(self): - return '%s %s' % (self.color_code, self.first_name, self.last_name) - colored_name.allow_tags = True + def colored_name(self): + return '%s %s' % (self.color_code, self.first_name, self.last_name) + colored_name.allow_tags = True - class PersonAdmin(admin.ModelAdmin): - list_display = ('first_name', 'last_name', 'colored_name') + class PersonAdmin(admin.ModelAdmin): + list_display = ('first_name', 'last_name', 'colored_name') - * If the string given is a method of the model, ``ModelAdmin`` or a - callable that returns True or False Django will display a pretty "on" or - "off" icon if you give the method a ``boolean`` attribute whose value is - ``True``. + * If the string given is a method of the model, ``ModelAdmin`` or a + callable that returns True or False Django will display a pretty + "on" or "off" icon if you give the method a ``boolean`` attribute + whose value is ``True``. - Here's a full example model:: + Here's a full example model:: - class Person(models.Model): - first_name = models.CharField(max_length=50) - birthday = models.DateField() + class Person(models.Model): + first_name = models.CharField(max_length=50) + birthday = models.DateField() - def born_in_fifties(self): - return self.birthday.strftime('%Y')[:3] == '195' - born_in_fifties.boolean = True + def born_in_fifties(self): + return self.birthday.strftime('%Y')[:3] == '195' + born_in_fifties.boolean = True - class PersonAdmin(admin.ModelAdmin): - list_display = ('name', 'born_in_fifties') + class PersonAdmin(admin.ModelAdmin): + list_display = ('name', 'born_in_fifties') - * The ``__str__()`` and ``__unicode__()`` methods are just as valid in - ``list_display`` as any other model method, so it's perfectly OK to do - this:: + * The ``__str__()`` and ``__unicode__()`` methods are just as valid in + ``list_display`` as any other model method, so it's perfectly OK to + do this:: - list_display = ('__unicode__', 'some_other_field') + list_display = ('__unicode__', 'some_other_field') - * Usually, elements of ``list_display`` that aren't actual database fields - can't be used in sorting (because Django does all the sorting at the - database level). + * Usually, elements of ``list_display`` that aren't actual database + fields can't be used in sorting (because Django does all the sorting + at the database level). - However, if an element of ``list_display`` represents a certain database - field, you can indicate this fact by setting the ``admin_order_field`` - attribute of the item. + However, if an element of ``list_display`` represents a certain + database field, you can indicate this fact by setting the + ``admin_order_field`` attribute of the item. - For example:: + For example:: - class Person(models.Model): - first_name = models.CharField(max_length=50) - color_code = models.CharField(max_length=6) + class Person(models.Model): + first_name = models.CharField(max_length=50) + color_code = models.CharField(max_length=6) - def colored_first_name(self): - return '%s' % (self.color_code, self.first_name) - colored_first_name.allow_tags = True - colored_first_name.admin_order_field = 'first_name' + def colored_first_name(self): + return '%s' % (self.color_code, self.first_name) + colored_first_name.allow_tags = True + colored_first_name.admin_order_field = 'first_name' - class PersonAdmin(admin.ModelAdmin): - list_display = ('first_name', 'colored_first_name') + class PersonAdmin(admin.ModelAdmin): + list_display = ('first_name', 'colored_first_name') - The above will tell Django to order by the ``first_name`` field when - trying to sort by ``colored_first_name`` in the admin. + The above will tell Django to order by the ``first_name`` field when + trying to sort by ``colored_first_name`` in the admin. .. attribute:: ModelAdmin.list_display_links -Set ``list_display_links`` to control which fields in ``list_display`` should -be linked to the "change" page for an object. + Set ``list_display_links`` to control which fields in ``list_display`` + should be linked to the "change" page for an object. -By default, the change list page will link the first column -- the first field -specified in ``list_display`` -- to the change page for each item. But -``list_display_links`` lets you change which columns are linked. Set -``list_display_links`` to a list or tuple of field names (in the same format as -``list_display``) to link. + By default, the change list page will link the first column -- the first + field specified in ``list_display`` -- to the change page for each item. + But ``list_display_links`` lets you change which columns are linked. Set + ``list_display_links`` to a list or tuple of fields (in the same + format as ``list_display``) to link. -``list_display_links`` can specify one or many field names. As long as the -field names appear in ``list_display``, Django doesn't care how many (or how -few) fields are linked. The only requirement is: If you want to use -``list_display_links``, you must define ``list_display``. + ``list_display_links`` can specify one or many fields. As long as the + fields appear in ``list_display``, Django doesn't care how many (or + how few) fields are linked. The only requirement is: If you want to use + ``list_display_links``, you must define ``list_display``. -In this example, the ``first_name`` and ``last_name`` fields will be linked on -the change list page:: + In this example, the ``first_name`` and ``last_name`` fields will be + linked on the change list page:: - class PersonAdmin(admin.ModelAdmin): - list_display = ('first_name', 'last_name', 'birthday') - list_display_links = ('first_name', 'last_name') + class PersonAdmin(admin.ModelAdmin): + list_display = ('first_name', 'last_name', 'birthday') + list_display_links = ('first_name', 'last_name') -.. _admin-list-editable: + .. _admin-list-editable: .. attribute:: ModelAdmin.list_editable -.. versionadded:: 1.1 + .. versionadded:: 1.1 -Set ``list_editable`` to a list of field names on the model which will allow -editing on the change list page. That is, fields listed in ``list_editable`` -will be displayed as form widgets on the change list page, allowing users to -edit and save multiple rows at once. + Set ``list_editable`` to a list of field names on the model which will + allow editing on the change list page. That is, fields listed in + ``list_editable`` will be displayed as form widgets on the change list + page, allowing users to edit and save multiple rows at once. -.. note:: + .. note:: - ``list_editable`` interacts with a couple of other options in particular - ways; you should note the following rules: + ``list_editable`` interacts with a couple of other options in + particular ways; you should note the following rules: - * Any field in ``list_editable`` must also be in ``list_display``. You - can't edit a field that's not displayed! + * Any field in ``list_editable`` must also be in ``list_display``. + You can't edit a field that's not displayed! - * The same field can't be listed in both ``list_editable`` and - ``list_display_links`` -- a field can't be both a form and a link. + * The same field can't be listed in both ``list_editable`` and + ``list_display_links`` -- a field can't be both a form and + a link. - You'll get a validation error if either of these rules are broken. + You'll get a validation error if either of these rules are broken. .. attribute:: ModelAdmin.list_filter -Set ``list_filter`` to activate filters in the right sidebar of the change list -page of the admin. This should be a list of field names, and each specified -field should be either a ``BooleanField``, ``CharField``, ``DateField``, -``DateTimeField``, ``IntegerField`` or ``ForeignKey``. + Set ``list_filter`` to activate filters in the right sidebar of the change + list page of the admin. This should be a list of field names, and each + specified field should be either a ``BooleanField``, ``CharField``, + ``DateField``, ``DateTimeField``, ``IntegerField`` or ``ForeignKey``. -This example, taken from the ``django.contrib.auth.models.User`` model, shows -how both ``list_display`` and ``list_filter`` work:: + This example, taken from the ``django.contrib.auth.models.User`` model, + shows how both ``list_display`` and ``list_filter`` work:: - class UserAdmin(admin.ModelAdmin): - list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_filter = ('is_staff', 'is_superuser') + class UserAdmin(admin.ModelAdmin): + list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') + list_filter = ('is_staff', 'is_superuser') -The above code results in an admin change list page that looks like this: + The above code results in an admin change list page that looks like this: - .. image:: _images/users_changelist.png + .. image:: _images/users_changelist.png -(This example also has ``search_fields`` defined. See below.) + (This example also has ``search_fields`` defined. See below.) .. attribute:: ModelAdmin.list_per_page -Set ``list_per_page`` to control how many items appear on each paginated admin -change list page. By default, this is set to ``100``. + Set ``list_per_page`` to control how many items appear on each paginated + admin change list page. By default, this is set to ``100``. .. attribute:: ModelAdmin.list_select_related -Set ``list_select_related`` to tell Django to use ``select_related()`` in -retrieving 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``. + Set ``list_select_related`` to tell Django to use + :meth:`~django.db.models.QuerySet.select_related` in retrieving the list of + objects on the admin change list page. This can save you a bunch of + database queries. -Note that Django will use ``select_related()``, regardless of this setting, -if one of the ``list_display`` fields is a ``ForeignKey``. + The value should be either ``True`` or ``False``. Default is ``False``. -For more on ``select_related()``, see -:ref:`the select_related() docs `. + Note that Django will use :meth:`~django.db.models.QuerySet.select_related`, + regardless of this setting, if one of the ``list_display`` fields is a + ``ForeignKey``. .. attribute:: ModelAdmin.inlines -See ``InlineModelAdmin`` objects below. + See ``InlineModelAdmin`` objects below. .. attribute:: ModelAdmin.ordering -Set ``ordering`` to specify how objects on the admin change list page should be -ordered. This should be a list or tuple in the same format as a model's -``ordering`` parameter. + Set ``ordering`` to specify how lists of objects should be ordered in the + Django admin views. This should be a list or tuple in the same format as a + model's ``ordering`` parameter. -If this isn't provided, the Django admin will use the model's default ordering. + If this isn't provided, the Django admin will use the model's default + ordering. -.. admonition:: Note + .. admonition:: Note - Django will only honor the first element in the list/tuple; any others - will be ignored. + Django will only honor the first element in the list/tuple; any others + will be ignored. .. attribute:: ModelAdmin.prepopulated_fields -Set ``prepopulated_fields`` to a dictionary mapping field names to the fields -it should prepopulate from:: + Set ``prepopulated_fields`` to a dictionary mapping field names to the + fields it should prepopulate from:: - class ArticleAdmin(admin.ModelAdmin): - prepopulated_fields = {"slug": ("title",)} + class ArticleAdmin(admin.ModelAdmin): + prepopulated_fields = {"slug": ("title",)} -When set, the given fields will use a bit of JavaScript to populate from the -fields assigned. The main use for this functionality is to automatically -generate the value for ``SlugField`` fields from one or more other fields. The -generated value is produced by concatenating the values of the source fields, -and then by transforming that result into a valid slug (e.g. substituting -dashes for spaces). + When set, the given fields will use a bit of JavaScript to populate from + the fields assigned. The main use for this functionality is to + automatically generate the value for ``SlugField`` fields from one or more + other fields. The generated value is produced by concatenating the values + of the source fields, and then by transforming that result into a valid + slug (e.g. substituting dashes for spaces). -``prepopulated_fields`` doesn't accept ``DateTimeField``, ``ForeignKey``, nor -``ManyToManyField`` fields. + ``prepopulated_fields`` doesn't accept ``DateTimeField``, ``ForeignKey``, + nor ``ManyToManyField`` fields. .. attribute:: ModelAdmin.radio_fields -By default, Django's admin uses a select-box interface () for + fields that are ``ForeignKey`` or have ``choices`` set. If a field is + present in ``radio_fields``, Django will use a radio-button interface + instead. Assuming ``group`` is a ``ForeignKey`` on the ``Person`` model:: - class PersonAdmin(admin.ModelAdmin): - radio_fields = {"group": admin.VERTICAL} + class PersonAdmin(admin.ModelAdmin): + radio_fields = {"group": admin.VERTICAL} -You have the choice of using ``HORIZONTAL`` or ``VERTICAL`` from the -``django.contrib.admin`` module. + You have the choice of using ``HORIZONTAL`` or ``VERTICAL`` from the + ``django.contrib.admin`` module. -Don't include a field in ``radio_fields`` unless it's a ``ForeignKey`` or has -``choices`` set. + Don't include a field in ``radio_fields`` unless it's a ``ForeignKey`` or has + ``choices`` set. .. attribute:: ModelAdmin.raw_id_fields -By default, Django's admin uses a select-box interface () for + fields that are ``ForeignKey``. Sometimes you don't want to incur the + overhead of having to select all the related instances to display in the + drop-down. -``raw_id_fields`` is a list of fields you would like to change -into a ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``:: + ``raw_id_fields`` is a list of fields you would like to change + into a ``Input`` widget for either a ``ForeignKey`` or + ``ManyToManyField``:: - class ArticleAdmin(admin.ModelAdmin): - raw_id_fields = ("newspaper",) + class ArticleAdmin(admin.ModelAdmin): + raw_id_fields = ("newspaper",) .. attribute:: ModelAdmin.readonly_fields -.. versionadded:: 1.2 + .. versionadded:: 1.2 -By default the admin shows all fields as editable. Any fields in this option -(which should be a ``list`` or ``tuple``) will display its data as-is and -non-editable. This option behaves nearly identical to :attr:`ModelAdmin.list_display`. -Usage is the same, however, when you specify :attr:`ModelAdmin.fields` or -:attr:`ModelAdmin.fieldsets` the read-only fields must be present to be shown -(they are ignored otherwise). + By default the admin shows all fields as editable. Any fields in this + option (which should be a ``list`` or ``tuple``) will display its data + as-is and non-editable. This option behaves nearly identical to + :attr:`ModelAdmin.list_display`. Usage is the same, however, when you + specify :attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets` the + read-only fields must be present to be shown (they are ignored otherwise). -If ``readonly_fields`` is used without defining explicit ordering through -:attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets` they will be added -last after all editable fields. + If ``readonly_fields`` is used without defining explicit ordering through + :attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets` they will be + added last after all editable fields. .. attribute:: ModelAdmin.save_as -Set ``save_as`` to enable a "save as" feature on admin change forms. + Set ``save_as`` to enable a "save as" feature on admin change forms. -Normally, objects have three save options: "Save", "Save and continue editing" -and "Save and add another". If ``save_as`` is ``True``, "Save and add another" -will be replaced by a "Save as" button. + Normally, objects have three save options: "Save", "Save and continue + editing" and "Save and add another". If ``save_as`` is ``True``, "Save + and add another" will be replaced by a "Save as" button. -"Save as" means the object will be saved as a new object (with a new ID), -rather than the old object. + "Save as" means the object will be saved as a new object (with a new ID), + rather than the old object. -By default, ``save_as`` is set to ``False``. + By default, ``save_as`` is set to ``False``. .. attribute:: ModelAdmin.save_on_top -Set ``save_on_top`` to add save buttons across the top of your admin change -forms. + Set ``save_on_top`` to add save buttons across the top of your adminchange + forms. -Normally, the save buttons appear only at the bottom of the forms. If you set -``save_on_top``, the buttons will appear both on the top and the bottom. + Normally, the save buttons appear only at the bottom of the forms. If you + set ``save_on_top``, the buttons will appear both on the top and the + bottom. By default, ``save_on_top`` is set to ``False``. .. attribute:: ModelAdmin.search_fields -Set ``search_fields`` to enable a search box on the admin change list page. -This should be set to a list of field names that will be searched whenever -somebody submits a search query in that text box. + Set ``search_fields`` to enable a search box on the admin change list page. + This should be set to a list of field names that will be searched whenever + somebody submits a search query in that text box. + + These fields should be some kind of text field, such as ``CharField`` or + ``TextField``. You can also perform a related lookup on a ``ForeignKey`` or + ``ManyToManyField`` with the lookup API "follow" notation:: + + search_fields = ['foreign_key__related_fieldname'] -These fields should be some kind of text field, such as ``CharField`` or -``TextField``. You can also perform a related lookup on a ``ForeignKey`` with -the lookup API "follow" notation:: + For example, if you have a blog entry with an author, the following + definition would enable search blog entries by the email address of the + author:: - search_fields = ['foreign_key__related_fieldname'] + search_fields = ['user__email'] -When somebody does a search in the admin search box, Django splits the search -query into words and returns all objects that contain each of the words, case -insensitive, where each word must be in at least one of ``search_fields``. For -example, if ``search_fields`` is set to ``['first_name', 'last_name']`` and a -user searches for ``john lennon``, Django will do the equivalent of this SQL -``WHERE`` clause:: + When somebody does a search in the admin search box, Django splits the + search query into words and returns all objects that contain each of the + words, case insensitive, where each word must be in at least one of + ``search_fields``. For example, if ``search_fields`` is set to + ``['first_name', 'last_name']`` and a user searches for ``john lennon``, + Django will do the equivalent of this SQL ``WHERE`` clause:: - WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%') - AND (first_name ILIKE '%lennon%' OR last_name ILIKE '%lennon%') + WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%') + AND (first_name ILIKE '%lennon%' OR last_name ILIKE '%lennon%') -For faster and/or more restrictive searches, prefix the field name -with an operator: + For faster and/or more restrictive searches, prefix the field name + with an operator: -``^`` - Matches the beginning of the field. For example, if ``search_fields`` is - set to ``['^first_name', '^last_name']`` and a user searches for - ``john lennon``, Django will do the equivalent of this SQL ``WHERE`` - clause:: + ``^`` + Matches the beginning of the field. For example, if ``search_fields`` + is set to ``['^first_name', '^last_name']`` and a user searches for + ``john lennon``, Django will do the equivalent of this SQL ``WHERE`` + clause:: - WHERE (first_name ILIKE 'john%' OR last_name ILIKE 'john%') - AND (first_name ILIKE 'lennon%' OR last_name ILIKE 'lennon%') + WHERE (first_name ILIKE 'john%' OR last_name ILIKE 'john%') + AND (first_name ILIKE 'lennon%' OR last_name ILIKE 'lennon%') - This query is more efficient than the normal ``'%john%'`` query, because - the database only needs to check the beginning of a column's data, rather - than seeking through the entire column's data. Plus, if the column has an - index on it, some databases may be able to use the index for this query, - even though it's a ``LIKE`` query. + This query is more efficient than the normal ``'%john%'`` query, + because the database only needs to check the beginning of a column's + data, rather than seeking through the entire column's data. Plus, if + the column has an index on it, some databases may be able to use the + index for this query, even though it's a ``LIKE`` query. -``=`` - Matches exactly, case-insensitive. For example, if - ``search_fields`` is set to ``['=first_name', '=last_name']`` and - a user searches for ``john lennon``, Django will do the equivalent - of this SQL ``WHERE`` clause:: + ``=`` + Matches exactly, case-insensitive. For example, if + ``search_fields`` is set to ``['=first_name', '=last_name']`` and + a user searches for ``john lennon``, Django will do the equivalent + of this SQL ``WHERE`` clause:: - WHERE (first_name ILIKE 'john' OR last_name ILIKE 'john') - AND (first_name ILIKE 'lennon' OR last_name ILIKE 'lennon') + WHERE (first_name ILIKE 'john' OR last_name ILIKE 'john') + AND (first_name ILIKE 'lennon' OR last_name ILIKE 'lennon') - Note that the query input is split by spaces, so, following this example, - it's currently not possible to search for all records in which - ``first_name`` is exactly ``'john winston'`` (containing a space). + Note that the query input is split by spaces, so, following this + example, it's currently not possible to search for all records in which + ``first_name`` is exactly ``'john winston'`` (containing a space). -``@`` - Performs a full-text match. This is like the default search method but uses - an index. Currently this is only available for MySQL. + ``@`` + Performs a full-text match. This is like the default search method but + uses an index. Currently this is only available for MySQL. .. attribute:: ModelAdmin.formfield_overrides -.. versionadded:: 1.1 + .. versionadded:: 1.1 -This provides a quick-and-dirty way to override some of the -:class:`~django.forms.Field` options for use in the admin. -``formfield_overrides`` is a dictionary mapping a field class to a dict of -arguments to pass to the field at construction time. + This provides a quick-and-dirty way to override some of the + :class:`~django.forms.Field` options for use in the admin. + ``formfield_overrides`` is a dictionary mapping a field class to a dict of + arguments to pass to the field at construction time. -Since that's a bit abstract, let's look at a concrete example. The most common -use of ``formfield_overrides`` is to add a custom widget for a certain type of -field. So, imagine we've written a ``RichTextEditorWidget`` that we'd like to -use for large text fields instead of the default ``
        You can restrict a form to a subset of the complete list of fields @@ -583,8 +595,9 @@ class CustomFieldForExclusionModel(models.Model): >>> art = Article(headline='Test article', slug='test-article', pub_date=datetime.date(1988, 1, 4), writer=w, article='Hello.') >>> art.save() ->>> art.id -1 +>>> art_id_1 = art.id +>>> art_id_1 is not None +True >>> class TestArticleForm(ModelForm): ... class Meta: ... model = Article @@ -595,8 +608,8 @@ class CustomFieldForExclusionModel(models.Model):
      • Pub date:
      • Writer:
      • Article:
      • Status:
      • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
      • >>> f = TestArticleForm({'headline': u'Test headline', 'slug': 'test-headline', 'pub_date': u'1984-02-06', 'writer': unicode(w_royko.pk), 'article': 'Hello.'}, instance=art) >>> f.errors @@ -616,9 +629,9 @@ class CustomFieldForExclusionModel(models.Model): >>> f.is_valid() True >>> test_art = f.save() ->>> test_art.id -1 ->>> test_art = Article.objects.get(id=1) +>>> test_art.id == art_id_1 +True +>>> test_art = Article.objects.get(id=art_id_1) >>> test_art.headline u'Test headline' @@ -636,9 +649,9 @@ class CustomFieldForExclusionModel(models.Model): >>> f.is_valid() True >>> new_art = f.save() ->>> new_art.id -1 ->>> new_art = Article.objects.get(id=1) +>>> new_art.id == art_id_1 +True +>>> new_art = Article.objects.get(id=art_id_1) >>> new_art.headline u'New headline' @@ -658,8 +671,8 @@ class CustomFieldForExclusionModel(models.Model):
      • Pub date:
      • Writer:
      • Article:
      • Status:
      • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
      • Initial values can be provided for model forms ->>> f = TestArticleForm(auto_id=False, initial={'headline': 'Your headline here', 'categories': ['1','2']}) +>>> f = TestArticleForm(auto_id=False, initial={'headline': 'Your headline here', 'categories': [str(c1.id), str(c2.id)]}) >>> print f.as_ul()
      • Headline:
      • Slug:
      • Pub date:
      • Writer:
      • Article:
      • Status:
      • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
      • >>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04', -... 'writer': unicode(w_royko.pk), 'article': u'Hello.', 'categories': [u'1', u'2']}, instance=new_art) +... 'writer': unicode(w_royko.pk), 'article': u'Hello.', 'categories': [unicode(c1.id), unicode(c2.id)]}, instance=new_art) >>> new_art = f.save() ->>> new_art.id -1 ->>> new_art = Article.objects.get(id=1) +>>> new_art.id == art_id_1 +True +>>> new_art = Article.objects.get(id=art_id_1) >>> new_art.categories.order_by('name') [, ] @@ -711,9 +724,9 @@ class CustomFieldForExclusionModel(models.Model): >>> f = TestArticleForm({'headline': u'New headline', 'slug': u'new-headline', 'pub_date': u'1988-01-04', ... 'writer': unicode(w_royko.pk), 'article': u'Hello.'}, instance=new_art) >>> new_art = f.save() ->>> new_art.id -1 ->>> new_art = Article.objects.get(id=1) +>>> new_art.id == art_id_1 +True +>>> new_art = Article.objects.get(id=art_id_1) >>> new_art.categories.all() [] @@ -722,11 +735,12 @@ class CustomFieldForExclusionModel(models.Model): ... class Meta: ... model = Article >>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01', -... 'writer': unicode(w_royko.pk), 'article': u'Test.', 'categories': [u'1', u'2']}) +... 'writer': unicode(w_royko.pk), 'article': u'Test.', 'categories': [unicode(c1.id), unicode(c2.id)]}) >>> new_art = f.save() ->>> new_art.id -2 ->>> new_art = Article.objects.get(id=2) +>>> art_id_2 = new_art.id +>>> art_id_2 not in (None, art_id_1) +True +>>> new_art = Article.objects.get(id=art_id_2) >>> new_art.categories.order_by('name') [, ] @@ -737,9 +751,10 @@ class CustomFieldForExclusionModel(models.Model): >>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': u'walrus-was-paul', 'pub_date': u'1967-11-01', ... 'writer': unicode(w_royko.pk), 'article': u'Test.'}) >>> new_art = f.save() ->>> new_art.id -3 ->>> new_art = Article.objects.get(id=3) +>>> art_id_3 = new_art.id +>>> art_id_3 not in (None, art_id_1, art_id_2) +True +>>> new_art = Article.objects.get(id=art_id_3) >>> new_art.categories.all() [] @@ -749,16 +764,17 @@ class CustomFieldForExclusionModel(models.Model): ... class Meta: ... model = Article >>> f = ArticleForm({'headline': u'The walrus was Paul', 'slug': 'walrus-was-paul', 'pub_date': u'1967-11-01', -... 'writer': unicode(w_royko.pk), 'article': u'Test.', 'categories': [u'1', u'2']}) +... 'writer': unicode(w_royko.pk), 'article': u'Test.', 'categories': [unicode(c1.id), unicode(c2.id)]}) >>> new_art = f.save(commit=False) # Manually save the instance >>> new_art.save() ->>> new_art.id -4 +>>> art_id_4 = new_art.id +>>> art_id_4 not in (None, art_id_1, art_id_2, art_id_3) +True # The instance doesn't have m2m data yet ->>> new_art = Article.objects.get(id=4) +>>> new_art = Article.objects.get(id=art_id_4) >>> new_art.categories.all() [] @@ -777,12 +793,12 @@ class CustomFieldForExclusionModel(models.Model): >>> cat = Category.objects.get(name='Third test') >>> cat ->>> cat.id -3 +>>> cat.id == c3.id +True >>> form = ShortCategory({'name': 'Third', 'slug': 'third', 'url': '3rd'}, instance=cat) >>> form.save() ->>> Category.objects.get(id=3) +>>> Category.objects.get(id=c3.id) Here, we demonstrate that choices for a ForeignKey ChoiceField are determined @@ -798,8 +814,8 @@ class CustomFieldForExclusionModel(models.Model):
      • Pub date:
      • Writer:
      • Article:
      • Status:
      • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
      • ->>> Category.objects.create(name='Fourth', url='4th') +>>> c4 = Category.objects.create(name='Fourth', url='4th') +>>> c4 >>> Writer.objects.create(name='Carl Bernstein') @@ -823,9 +840,9 @@ class CustomFieldForExclusionModel(models.Model):
      • Pub date:
      • Writer:
      • Article:
      • Status:
      • Categories: Hold down "Control", or "Command" on a Mac, to select more than one.
      • # ModelChoiceField ############################################################ @@ -847,7 +864,7 @@ class CustomFieldForExclusionModel(models.Model): >>> f = ModelChoiceField(Category.objects.all()) >>> list(f.choices) -[(u'', u'---------'), (1, u'Entertainment'), (2, u"It's a test"), (3, u'Third'), (4, u'Fourth')] +[(u'', u'---------'), (..., u'Entertainment'), (..., u"It's a test"), (..., u'Third'), (..., u'Fourth')] >>> f.clean('') Traceback (most recent call last): ... @@ -860,33 +877,34 @@ class CustomFieldForExclusionModel(models.Model): Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] ->>> f.clean(3) +>>> f.clean(c3.id) ->>> f.clean(2) +>>> f.clean(c2.id) # Add a Category object *after* the ModelChoiceField has already been # instantiated. This proves clean() checks the database during clean() rather # than caching it at time of instantiation. ->>> Category.objects.create(name='Fifth', url='5th') +>>> c5 = Category.objects.create(name='Fifth', url='5th') +>>> c5 ->>> f.clean(5) +>>> f.clean(c5.id) # Delete a Category object *after* the ModelChoiceField has already been # instantiated. This proves clean() checks the database during clean() rather # than caching it at time of instantiation. >>> Category.objects.get(url='5th').delete() ->>> f.clean(5) +>>> f.clean(c5.id) Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] ->>> f = ModelChoiceField(Category.objects.filter(pk=1), required=False) +>>> f = ModelChoiceField(Category.objects.filter(pk=c1.id), required=False) >>> print f.clean('') None >>> f.clean('') ->>> f.clean('1') +>>> f.clean(str(c1.id)) >>> f.clean('100') Traceback (most recent call last): @@ -896,10 +914,10 @@ class CustomFieldForExclusionModel(models.Model): # queryset can be changed after the field is created. >>> f.queryset = Category.objects.exclude(name='Fourth') >>> list(f.choices) -[(u'', u'---------'), (1, u'Entertainment'), (2, u"It's a test"), (3, u'Third')] ->>> f.clean(3) +[(u'', u'---------'), (..., u'Entertainment'), (..., u"It's a test"), (..., u'Third')] +>>> f.clean(c3.id) ->>> f.clean(4) +>>> f.clean(c4.id) Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. That choice is not one of the available choices.'] @@ -908,21 +926,21 @@ class CustomFieldForExclusionModel(models.Model): >>> gen_one = list(f.choices) >>> gen_two = f.choices >>> gen_one[2] -(2L, u"It's a test") +(..., u"It's a test") >>> list(gen_two) -[(u'', u'---------'), (1L, u'Entertainment'), (2L, u"It's a test"), (3L, u'Third')] +[(u'', u'---------'), (..., u'Entertainment'), (..., u"It's a test"), (..., u'Third')] # check that we can override the label_from_instance method to print custom labels (#4620) >>> f.queryset = Category.objects.all() >>> f.label_from_instance = lambda obj: "category " + str(obj) >>> list(f.choices) -[(u'', u'---------'), (1L, 'category Entertainment'), (2L, "category It's a test"), (3L, 'category Third'), (4L, 'category Fourth')] +[(u'', u'---------'), (..., 'category Entertainment'), (..., "category It's a test"), (..., 'category Third'), (..., 'category Fourth')] # ModelMultipleChoiceField #################################################### >>> f = ModelMultipleChoiceField(Category.objects.all()) >>> list(f.choices) -[(1, u'Entertainment'), (2, u"It's a test"), (3, u'Third'), (4, u'Fourth')] +[(..., u'Entertainment'), (..., u"It's a test"), (..., u'Third'), (..., u'Fourth')] >>> f.clean(None) Traceback (most recent call last): ... @@ -931,17 +949,17 @@ class CustomFieldForExclusionModel(models.Model): Traceback (most recent call last): ... ValidationError: [u'This field is required.'] ->>> f.clean([1]) +>>> f.clean([c1.id]) [] ->>> f.clean([2]) +>>> f.clean([c2.id]) [] ->>> f.clean(['1']) +>>> f.clean([str(c1.id)]) [] ->>> f.clean(['1', '2']) +>>> f.clean([str(c1.id), str(c2.id)]) [, ] ->>> f.clean([1, '2']) +>>> f.clean([c1.id, str(c2.id)]) [, ] ->>> f.clean((1, '2')) +>>> f.clean((c1.id, str(c2.id))) [, ] >>> f.clean(['100']) Traceback (most recent call last): @@ -959,16 +977,17 @@ class CustomFieldForExclusionModel(models.Model): # Add a Category object *after* the ModelMultipleChoiceField has already been # instantiated. This proves clean() checks the database during clean() rather # than caching it at time of instantiation. ->>> Category.objects.create(id=6, name='Sixth', url='6th') +>>> c6 = Category.objects.create(id=6, name='Sixth', url='6th') +>>> c6 ->>> f.clean([6]) +>>> f.clean([c6.id]) [] # Delete a Category object *after* the ModelMultipleChoiceField has already been # instantiated. This proves clean() checks the database during clean() rather # than caching it at time of instantiation. >>> Category.objects.get(url='6th').delete() ->>> f.clean([6]) +>>> f.clean([c6.id]) Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 6 is not one of the available choices.'] @@ -982,11 +1001,11 @@ class CustomFieldForExclusionModel(models.Model): Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 10 is not one of the available choices.'] ->>> f.clean(['3', '10']) +>>> f.clean([str(c3.id), '10']) Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 10 is not one of the available choices.'] ->>> f.clean(['1', '10']) +>>> f.clean([str(c1.id), '10']) Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. 10 is not one of the available choices.'] @@ -994,22 +1013,22 @@ class CustomFieldForExclusionModel(models.Model): # queryset can be changed after the field is created. >>> f.queryset = Category.objects.exclude(name='Fourth') >>> list(f.choices) -[(1, u'Entertainment'), (2, u"It's a test"), (3, u'Third')] ->>> f.clean([3]) +[(..., u'Entertainment'), (..., u"It's a test"), (..., u'Third')] +>>> f.clean([c3.id]) [] ->>> f.clean([4]) +>>> f.clean([c4.id]) Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. 4 is not one of the available choices.'] ->>> f.clean(['3', '4']) +ValidationError: [u'Select a valid choice. ... is not one of the available choices.'] +>>> f.clean([str(c3.id), str(c4.id)]) Traceback (most recent call last): ... -ValidationError: [u'Select a valid choice. 4 is not one of the available choices.'] +ValidationError: [u'Select a valid choice. ... is not one of the available choices.'] >>> f.queryset = Category.objects.all() >>> f.label_from_instance = lambda obj: "multicategory " + str(obj) >>> list(f.choices) -[(1L, 'multicategory Entertainment'), (2L, "multicategory It's a test"), (3L, 'multicategory Third'), (4L, 'multicategory Fourth')] +[(..., 'multicategory Entertainment'), (..., "multicategory It's a test"), (..., 'multicategory Third'), (..., 'multicategory Fourth')] # OneToOneField ############################################################### @@ -1047,10 +1066,10 @@ class CustomFieldForExclusionModel(models.Model): >>> print form.as_p()

        @@ -1067,10 +1086,10 @@ class CustomFieldForExclusionModel(models.Model): >>> print form.as_p()

        @@ -1521,8 +1540,8 @@ class CustomFieldForExclusionModel(models.Model): ... print choice (u'', u'---------') (86, u'Apple') -(22, u'Pear') (87, u'Core') +(22, u'Pear') >>> class InventoryForm(ModelForm): ... class Meta: @@ -1532,8 +1551,8 @@ class CustomFieldForExclusionModel(models.Model): >>> data = model_to_dict(core) @@ -1556,6 +1575,25 @@ class CustomFieldForExclusionModel(models.Model):
        +# to_field_name should also work on ModelMultipleChoiceField ################## + +>>> field = ModelMultipleChoiceField(Inventory.objects.all(), to_field_name='barcode') +>>> for choice in field.choices: +... print choice +(86, u'Apple') +(87, u'Core') +(22, u'Pear') +>>> field.clean([86]) +[] + +>>> class SelectInventoryForm(forms.Form): +... items = ModelMultipleChoiceField(Inventory.objects.all(), to_field_name='barcode') +>>> form = SelectInventoryForm({'items': [87, 22]}) +>>> form.is_valid() +True +>>> form.cleaned_data +{'items': [, ]} + # Model field that returns None to exclude itself with explicit fields ######## >>> class CustomFieldForExclusionForm(ModelForm): diff --git a/tests/modeltests/model_forms/tests.py b/tests/modeltests/model_forms/tests.py index 6a5f9395cc13..33918ee88cd9 100644 --- a/tests/modeltests/model_forms/tests.py +++ b/tests/modeltests/model_forms/tests.py @@ -1,9 +1,10 @@ import datetime from django.test import TestCase from django import forms -from models import Category, Writer, Book, DerivedBook, Post -from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm, - ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm) +from models import Category, Writer, Book, DerivedBook, Post, FlexibleDatePost +from mforms import (ProductForm, PriceForm, BookForm, DerivedBookForm, + ExplicitPKForm, PostForm, DerivedPostForm, CustomWriterForm, + FlexDatePostForm) class IncompleteCategoryFormWithFields(forms.ModelForm): @@ -156,6 +157,10 @@ def test_unique_for_date(self): form = PostForm({'subtitle': "Finally", "title": "Django 1.0 is released", "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p) self.assertTrue(form.is_valid()) + form = PostForm({'title': "Django 1.0 is released"}) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertEqual(form.errors['posted'], [u'This field is required.']) def test_inherited_unique_for_date(self): p = Post.objects.create(title="Django 1.0 is released", @@ -179,3 +184,16 @@ def test_inherited_unique_for_date(self): "slug": "Django 1.0", 'posted': '2008-09-03'}, instance=p) self.assertTrue(form.is_valid()) + def test_unique_for_date_with_nullable_date(self): + p = FlexibleDatePost.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + + form = FlexDatePostForm({'title': "Django 1.0 is released"}) + self.assertTrue(form.is_valid()) + form = FlexDatePostForm({'slug': "Django 1.0"}) + self.assertTrue(form.is_valid()) + form = FlexDatePostForm({'subtitle': "Finally"}) + self.assertTrue(form.is_valid()) + form = FlexDatePostForm({'subtitle': "Finally", "title": "Django 1.0 is released", + "slug": "Django 1.0"}, instance=p) + self.assertTrue(form.is_valid()) diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py index d5825313ec12..3eca6964d154 100644 --- a/tests/modeltests/model_formsets/models.py +++ b/tests/modeltests/model_formsets/models.py @@ -1,5 +1,4 @@ import datetime -from django import forms from django.db import models class Author(models.Model): @@ -35,6 +34,23 @@ class BookWithCustomPK(models.Model): def __unicode__(self): return u'%s: %s' % (self.my_pk, self.title) +class Editor(models.Model): + name = models.CharField(max_length=100) + +class BookWithOptionalAltEditor(models.Model): + author = models.ForeignKey(Author) + # Optional secondary author + alt_editor = models.ForeignKey(Editor, blank=True, null=True) + title = models.CharField(max_length=100) + + class Meta: + unique_together = ( + ('author', 'title', 'alt_editor'), + ) + + def __unicode__(self): + return self.title + class AlternateBook(Book): notes = models.CharField(max_length=100) @@ -175,1019 +191,3 @@ class Post(models.Model): def __unicode__(self): return self.name - -__test__ = {'API_TESTS': """ - ->>> from datetime import date - ->>> from django.forms.models import modelformset_factory - ->>> qs = Author.objects.all() ->>> AuthorFormSet = modelformset_factory(Author, extra=3) - ->>> formset = AuthorFormSet(queryset=qs) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        - ->>> data = { -... 'form-TOTAL_FORMS': '3', # the number of forms rendered -... 'form-INITIAL_FORMS': '0', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-name': 'Charles Baudelaire', -... 'form-1-name': 'Arthur Rimbaud', -... 'form-2-name': '', -... } - ->>> formset = AuthorFormSet(data=data, queryset=qs) ->>> formset.is_valid() -True - ->>> formset.save() -[, ] - ->>> for author in Author.objects.order_by('name'): -... print author.name -Arthur Rimbaud -Charles Baudelaire - - -Gah! We forgot Paul Verlaine. Let's create a formset to edit the existing -authors with an extra form to add him. We *could* pass in a queryset to -restrict the Author objects we edit, but in this case we'll use it to display -them in alphabetical order by name. - ->>> qs = Author.objects.order_by('name') ->>> AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=False) - ->>> formset = AuthorFormSet(queryset=qs) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        - - ->>> data = { -... 'form-TOTAL_FORMS': '3', # the number of forms rendered -... 'form-INITIAL_FORMS': '2', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-id': '2', -... 'form-0-name': 'Arthur Rimbaud', -... 'form-1-id': '1', -... 'form-1-name': 'Charles Baudelaire', -... 'form-2-name': 'Paul Verlaine', -... } - ->>> formset = AuthorFormSet(data=data, queryset=qs) ->>> formset.is_valid() -True - -# Only changed or new objects are returned from formset.save() ->>> formset.save() -[] - ->>> for author in Author.objects.order_by('name'): -... print author.name -Arthur Rimbaud -Charles Baudelaire -Paul Verlaine - - -This probably shouldn't happen, but it will. If an add form was marked for -deltetion, make sure we don't save that form. - ->>> qs = Author.objects.order_by('name') ->>> AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=True) - ->>> formset = AuthorFormSet(queryset=qs) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        -

        -

        -

        -

        -

        - ->>> data = { -... 'form-TOTAL_FORMS': '4', # the number of forms rendered -... 'form-INITIAL_FORMS': '3', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-id': '2', -... 'form-0-name': 'Arthur Rimbaud', -... 'form-1-id': '1', -... 'form-1-name': 'Charles Baudelaire', -... 'form-2-id': '3', -... 'form-2-name': 'Paul Verlaine', -... 'form-3-name': 'Walt Whitman', -... 'form-3-DELETE': 'on', -... } - ->>> formset = AuthorFormSet(data=data, queryset=qs) ->>> formset.is_valid() -True - -# No objects were changed or saved so nothing will come back. ->>> formset.save() -[] - ->>> for author in Author.objects.order_by('name'): -... print author.name -Arthur Rimbaud -Charles Baudelaire -Paul Verlaine - -Let's edit a record to ensure save only returns that one record. - ->>> data = { -... 'form-TOTAL_FORMS': '4', # the number of forms rendered -... 'form-INITIAL_FORMS': '3', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-id': '2', -... 'form-0-name': 'Walt Whitman', -... 'form-1-id': '1', -... 'form-1-name': 'Charles Baudelaire', -... 'form-2-id': '3', -... 'form-2-name': 'Paul Verlaine', -... 'form-3-name': '', -... 'form-3-DELETE': '', -... } - ->>> formset = AuthorFormSet(data=data, queryset=qs) ->>> formset.is_valid() -True - -# One record has changed. ->>> formset.save() -[] - -Test the behavior of commit=False and save_m2m - ->>> meeting = AuthorMeeting.objects.create(created=date.today()) ->>> meeting.authors = Author.objects.all() - -# create an Author instance to add to the meeting. ->>> new_author = Author.objects.create(name=u'John Steinbeck') - ->>> AuthorMeetingFormSet = modelformset_factory(AuthorMeeting, extra=1, can_delete=True) ->>> data = { -... 'form-TOTAL_FORMS': '2', # the number of forms rendered -... 'form-INITIAL_FORMS': '1', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-id': '1', -... 'form-0-name': '2nd Tuesday of the Week Meeting', -... 'form-0-authors': [2, 1, 3, 4], -... 'form-1-name': '', -... 'form-1-authors': '', -... 'form-1-DELETE': '', -... } ->>> formset = AuthorMeetingFormSet(data=data, queryset=AuthorMeeting.objects.all()) ->>> formset.is_valid() -True ->>> instances = formset.save(commit=False) ->>> for instance in instances: -... instance.created = date.today() -... instance.save() ->>> formset.save_m2m() ->>> instances[0].authors.all() -[, , , ] - -# delete the author we created to allow later tests to continue working. ->>> new_author.delete() - -Test the behavior of max_num with model formsets. It should allow all existing -related objects/inlines for a given object to be displayed, but not allow -the creation of new inlines beyond max_num. - ->>> qs = Author.objects.order_by('name') - ->>> AuthorFormSet = modelformset_factory(Author, max_num=None, extra=3) ->>> formset = AuthorFormSet(queryset=qs) ->>> len(formset.forms) -6 ->>> len(formset.extra_forms) -3 - ->>> AuthorFormSet = modelformset_factory(Author, max_num=4, extra=3) ->>> formset = AuthorFormSet(queryset=qs) ->>> len(formset.forms) -4 ->>> len(formset.extra_forms) -1 - ->>> AuthorFormSet = modelformset_factory(Author, max_num=0, extra=3) ->>> formset = AuthorFormSet(queryset=qs) ->>> len(formset.forms) -3 ->>> len(formset.extra_forms) -0 - ->>> AuthorFormSet = modelformset_factory(Author, max_num=None) ->>> formset = AuthorFormSet(queryset=qs) ->>> [x.name for x in formset.get_queryset()] -[u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman'] - ->>> AuthorFormSet = modelformset_factory(Author, max_num=0) ->>> formset = AuthorFormSet(queryset=qs) ->>> [x.name for x in formset.get_queryset()] -[u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman'] - ->>> AuthorFormSet = modelformset_factory(Author, max_num=4) ->>> formset = AuthorFormSet(queryset=qs) ->>> [x.name for x in formset.get_queryset()] -[u'Charles Baudelaire', u'Paul Verlaine', u'Walt Whitman'] - - -# ModelForm with a custom save method in a formset ########################### - ->>> class PoetForm(forms.ModelForm): -... def save(self, commit=True): -... # change the name to "Vladimir Mayakovsky" just to be a jerk. -... author = super(PoetForm, self).save(commit=False) -... author.name = u"Vladimir Mayakovsky" -... if commit: -... author.save() -... return author - ->>> PoetFormSet = modelformset_factory(Poet, form=PoetForm) - ->>> data = { -... 'form-TOTAL_FORMS': '3', # the number of forms rendered -... 'form-INITIAL_FORMS': '0', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-name': 'Walt Whitman', -... 'form-1-name': 'Charles Baudelaire', -... 'form-2-name': '', -... } - ->>> qs = Poet.objects.all() ->>> formset = PoetFormSet(data=data, queryset=qs) ->>> formset.is_valid() -True - ->>> formset.save() -[, ] - - -# Model inheritance in model formsets ######################################## - ->>> BetterAuthorFormSet = modelformset_factory(BetterAuthor) ->>> formset = BetterAuthorFormSet() ->>> for form in formset.forms: -... print form.as_p() -

        -

        - ->>> data = { -... 'form-TOTAL_FORMS': '1', # the number of forms rendered -... 'form-INITIAL_FORMS': '0', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-author_ptr': '', -... 'form-0-name': 'Ernest Hemingway', -... 'form-0-write_speed': '10', -... } - ->>> formset = BetterAuthorFormSet(data) ->>> formset.is_valid() -True ->>> formset.save() -[] ->>> hemingway_id = BetterAuthor.objects.get(name="Ernest Hemingway").pk - ->>> formset = BetterAuthorFormSet() ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        -

        - ->>> data = { -... 'form-TOTAL_FORMS': '2', # the number of forms rendered -... 'form-INITIAL_FORMS': '1', # the number of forms with initial data -... 'form-MAX_NUM_FORMS': '', # the max number of forms -... 'form-0-author_ptr': hemingway_id, -... 'form-0-name': 'Ernest Hemingway', -... 'form-0-write_speed': '10', -... 'form-1-author_ptr': '', -... 'form-1-name': '', -... 'form-1-write_speed': '', -... } - ->>> formset = BetterAuthorFormSet(data) ->>> formset.is_valid() -True ->>> formset.save() -[] - -# Inline Formsets ############################################################ - -We can also create a formset that is tied to a parent model. This is how the -admin system's edit inline functionality works. - ->>> from django.forms.models import inlineformset_factory - ->>> AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=3) ->>> author = Author.objects.get(name='Charles Baudelaire') - ->>> formset = AuthorBooksFormSet(instance=author) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        - ->>> data = { -... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered -... 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data -... 'book_set-MAX_NUM_FORMS': '', # the max number of forms -... 'book_set-0-title': 'Les Fleurs du Mal', -... 'book_set-1-title': '', -... 'book_set-2-title': '', -... } - ->>> formset = AuthorBooksFormSet(data, instance=author) ->>> formset.is_valid() -True - ->>> formset.save() -[] - ->>> for book in author.book_set.all(): -... print book.title -Les Fleurs du Mal - - -Now that we've added a book to Charles Baudelaire, let's try adding another -one. This time though, an edit form will be available for every existing -book. - ->>> AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) ->>> author = Author.objects.get(name='Charles Baudelaire') - ->>> formset = AuthorBooksFormSet(instance=author) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        - ->>> data = { -... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered -... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data -... 'book_set-MAX_NUM_FORMS': '', # the max number of forms -... 'book_set-0-id': '1', -... 'book_set-0-title': 'Les Fleurs du Mal', -... 'book_set-1-title': 'Les Paradis Artificiels', -... 'book_set-2-title': '', -... } - ->>> formset = AuthorBooksFormSet(data, instance=author) ->>> formset.is_valid() -True - ->>> formset.save() -[] - -As you can see, 'Les Paradis Artificiels' is now a book belonging to Charles Baudelaire. - ->>> for book in author.book_set.order_by('id'): -... print book.title -Les Fleurs du Mal -Les Paradis Artificiels - -The save_as_new parameter lets you re-associate the data to a new instance. -This is used in the admin for save_as functionality. - ->>> data = { -... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered -... 'book_set-INITIAL_FORMS': '2', # the number of forms with initial data -... 'book_set-MAX_NUM_FORMS': '', # the max number of forms -... 'book_set-0-id': '1', -... 'book_set-0-title': 'Les Fleurs du Mal', -... 'book_set-1-id': '2', -... 'book_set-1-title': 'Les Paradis Artificiels', -... 'book_set-2-title': '', -... } - ->>> formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True) ->>> formset.is_valid() -True - ->>> new_author = Author.objects.create(name='Charles Baudelaire') ->>> formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True) ->>> [book for book in formset.save() if book.author.pk == new_author.pk] -[, ] - -Test using a custom prefix on an inline formset. - ->>> formset = AuthorBooksFormSet(prefix="test") ->>> for form in formset.forms: -... print form.as_p() -

        -

        - -Test inline formsets where the inline-edited object has a custom primary key that is not the fk to the parent object. - ->>> AuthorBooksFormSet2 = inlineformset_factory(Author, BookWithCustomPK, can_delete=False, extra=1) - ->>> formset = AuthorBooksFormSet2(instance=author) ->>> for form in formset.forms: -... print form.as_p() -

        -

        - ->>> data = { -... 'bookwithcustompk_set-TOTAL_FORMS': '1', # the number of forms rendered -... 'bookwithcustompk_set-INITIAL_FORMS': '0', # the number of forms with initial data -... 'bookwithcustompk_set-MAX_NUM_FORMS': '', # the max number of forms -... 'bookwithcustompk_set-0-my_pk': '77777', -... 'bookwithcustompk_set-0-title': 'Les Fleurs du Mal', -... } - ->>> formset = AuthorBooksFormSet2(data, instance=author) ->>> formset.is_valid() -True - ->>> formset.save() -[] - ->>> for book in author.bookwithcustompk_set.all(): -... print book.title -Les Fleurs du Mal - -Test inline formsets where the inline-edited object uses multi-table inheritance, thus -has a non AutoField yet auto-created primary key. - ->>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1) - ->>> formset = AuthorBooksFormSet3(instance=author) ->>> for form in formset.forms: -... print form.as_p() -

        -

        - - ->>> data = { -... 'alternatebook_set-TOTAL_FORMS': '1', # the number of forms rendered -... 'alternatebook_set-INITIAL_FORMS': '0', # the number of forms with initial data -... 'alternatebook_set-MAX_NUM_FORMS': '', # the max number of forms -... 'alternatebook_set-0-title': 'Flowers of Evil', -... 'alternatebook_set-0-notes': 'English translation of Les Fleurs du Mal' -... } - ->>> formset = AuthorBooksFormSet3(data, instance=author) ->>> formset.is_valid() -True - ->>> formset.save() -[] - - -# ModelForm with a custom save method in an inline formset ################### - ->>> class PoemForm(forms.ModelForm): -... def save(self, commit=True): -... # change the name to "Brooklyn Bridge" just to be a jerk. -... poem = super(PoemForm, self).save(commit=False) -... poem.name = u"Brooklyn Bridge" -... if commit: -... poem.save() -... return poem - ->>> PoemFormSet = inlineformset_factory(Poet, Poem, form=PoemForm) - ->>> data = { -... 'poem_set-TOTAL_FORMS': '3', # the number of forms rendered -... 'poem_set-INITIAL_FORMS': '0', # the number of forms with initial data -... 'poem_set-MAX_NUM_FORMS': '', # the max number of forms -... 'poem_set-0-name': 'The Cloud in Trousers', -... 'poem_set-1-name': 'I', -... 'poem_set-2-name': '', -... } - ->>> poet = Poet.objects.create(name='Vladimir Mayakovsky') ->>> formset = PoemFormSet(data=data, instance=poet) ->>> formset.is_valid() -True - ->>> formset.save() -[, ] - -We can provide a custom queryset to our InlineFormSet: - ->>> custom_qs = Book.objects.order_by('-title') ->>> formset = AuthorBooksFormSet(instance=author, queryset=custom_qs) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        -

        -

        - ->>> data = { -... 'book_set-TOTAL_FORMS': '5', # the number of forms rendered -... 'book_set-INITIAL_FORMS': '3', # the number of forms with initial data -... 'book_set-MAX_NUM_FORMS': '', # the max number of forms -... 'book_set-0-id': '1', -... 'book_set-0-title': 'Les Fleurs du Mal', -... 'book_set-1-id': '2', -... 'book_set-1-title': 'Les Paradis Artificiels', -... 'book_set-2-id': '5', -... 'book_set-2-title': 'Flowers of Evil', -... 'book_set-3-title': 'Revue des deux mondes', -... 'book_set-4-title': '', -... } ->>> formset = AuthorBooksFormSet(data, instance=author, queryset=custom_qs) ->>> formset.is_valid() -True - ->>> custom_qs = Book.objects.filter(title__startswith='F') ->>> formset = AuthorBooksFormSet(instance=author, queryset=custom_qs) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        ->>> data = { -... 'book_set-TOTAL_FORMS': '3', # the number of forms rendered -... 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data -... 'book_set-MAX_NUM_FORMS': '', # the max number of forms -... 'book_set-0-id': '5', -... 'book_set-0-title': 'Flowers of Evil', -... 'book_set-1-title': 'Revue des deux mondes', -... 'book_set-2-title': '', -... } ->>> formset = AuthorBooksFormSet(data, instance=author, queryset=custom_qs) ->>> formset.is_valid() -True - - -# Test a custom primary key ################################################### - -We need to ensure that it is displayed - ->>> CustomPrimaryKeyFormSet = modelformset_factory(CustomPrimaryKey) ->>> formset = CustomPrimaryKeyFormSet() ->>> for form in formset.forms: -... print form.as_p() -

        -

        - -# Custom primary keys with ForeignKey, OneToOneField and AutoField ############ - ->>> place = Place(name=u'Giordanos', city=u'Chicago') ->>> place.save() - ->>> FormSet = inlineformset_factory(Place, Owner, extra=2, can_delete=False) ->>> formset = FormSet(instance=place) ->>> for form in formset.forms: -... print form.as_p() -

        -

        - ->>> data = { -... 'owner_set-TOTAL_FORMS': '2', -... 'owner_set-INITIAL_FORMS': '0', -... 'owner_set-MAX_NUM_FORMS': '', -... 'owner_set-0-auto_id': '', -... 'owner_set-0-name': u'Joe Perry', -... 'owner_set-1-auto_id': '', -... 'owner_set-1-name': '', -... } ->>> formset = FormSet(data, instance=place) ->>> formset.is_valid() -True ->>> formset.save() -[] - ->>> formset = FormSet(instance=place) ->>> for form in formset.forms: -... print form.as_p() -

        -

        -

        - ->>> data = { -... 'owner_set-TOTAL_FORMS': '3', -... 'owner_set-INITIAL_FORMS': '1', -... 'owner_set-MAX_NUM_FORMS': '', -... 'owner_set-0-auto_id': u'1', -... 'owner_set-0-name': u'Joe Perry', -... 'owner_set-1-auto_id': '', -... 'owner_set-1-name': u'Jack Berry', -... 'owner_set-2-auto_id': '', -... 'owner_set-2-name': '', -... } ->>> formset = FormSet(data, instance=place) ->>> formset.is_valid() -True ->>> formset.save() -[] - -# Ensure a custom primary key that is a ForeignKey or OneToOneField get rendered for the user to choose. - ->>> FormSet = modelformset_factory(OwnerProfile) ->>> formset = FormSet() ->>> for form in formset.forms: -... print form.as_p() -

        -

        - ->>> owner = Owner.objects.get(name=u'Joe Perry') ->>> FormSet = inlineformset_factory(Owner, OwnerProfile, max_num=1, can_delete=False) ->>> FormSet.max_num -1 ->>> formset = FormSet(instance=owner) ->>> for form in formset.forms: -... print form.as_p() -

        - ->>> data = { -... 'ownerprofile-TOTAL_FORMS': '1', -... 'ownerprofile-INITIAL_FORMS': '0', -... 'ownerprofile-MAX_NUM_FORMS': '1', -... 'ownerprofile-0-owner': '', -... 'ownerprofile-0-age': u'54', -... } ->>> formset = FormSet(data, instance=owner) ->>> formset.is_valid() -True ->>> formset.save() -[] ->>> formset = FormSet(instance=owner) ->>> for form in formset.forms: -... print form.as_p() -

        - ->>> data = { -... 'ownerprofile-TOTAL_FORMS': '1', -... 'ownerprofile-INITIAL_FORMS': '1', -... 'ownerprofile-MAX_NUM_FORMS': '1', -... 'ownerprofile-0-owner': u'1', -... 'ownerprofile-0-age': u'55', -... } ->>> formset = FormSet(data, instance=owner) ->>> formset.is_valid() -True ->>> formset.save() -[] - -# ForeignKey with unique=True should enforce max_num=1 - ->>> FormSet = inlineformset_factory(Place, Location, can_delete=False) ->>> FormSet.max_num -1 ->>> formset = FormSet(instance=place) ->>> for form in formset.forms: -... print form.as_p() -

        -

        - -# Foreign keys in parents ######################################## - ->>> from django.forms.models import _get_foreign_key - ->>> type(_get_foreign_key(Restaurant, Owner)) - ->>> type(_get_foreign_key(MexicanRestaurant, Owner)) - - -# unique/unique_together validation ########################################### - ->>> FormSet = modelformset_factory(Product, extra=1) ->>> data = { -... 'form-TOTAL_FORMS': '1', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... 'form-0-slug': 'car-red', -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -True ->>> formset.save() -[] - ->>> data = { -... 'form-TOTAL_FORMS': '1', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... 'form-0-slug': 'car-red', -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -False ->>> formset.errors -[{'slug': [u'Product with this Slug already exists.']}] - -# unique_together - ->>> FormSet = modelformset_factory(Price, extra=1) ->>> data = { -... 'form-TOTAL_FORMS': '1', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... 'form-0-price': u'12.00', -... 'form-0-quantity': '1', -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -True ->>> formset.save() -[] - ->>> data = { -... 'form-TOTAL_FORMS': '1', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... 'form-0-price': u'12.00', -... 'form-0-quantity': '1', -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -False ->>> formset.errors -[{'__all__': [u'Price with this Price and Quantity already exists.']}] - -# unique_together with inlineformset_factory -# Also see bug #8882. - ->>> repository = Repository.objects.create(name=u'Test Repo') ->>> FormSet = inlineformset_factory(Repository, Revision, extra=1) ->>> data = { -... 'revision_set-TOTAL_FORMS': '1', -... 'revision_set-INITIAL_FORMS': '0', -... 'revision_set-MAX_NUM_FORMS': '', -... 'revision_set-0-repository': repository.pk, -... 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', -... 'revision_set-0-DELETE': '', -... } ->>> formset = FormSet(data, instance=repository) ->>> formset.is_valid() -True ->>> formset.save() -[] - -# attempt to save the same revision against against the same repo. ->>> data = { -... 'revision_set-TOTAL_FORMS': '1', -... 'revision_set-INITIAL_FORMS': '0', -... 'revision_set-MAX_NUM_FORMS': '', -... 'revision_set-0-repository': repository.pk, -... 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', -... 'revision_set-0-DELETE': '', -... } ->>> formset = FormSet(data, instance=repository) ->>> formset.is_valid() -False ->>> formset.errors -[{'__all__': [u'Revision with this Repository and Revision already exists.']}] - -# unique_together with inlineformset_factory with overridden form fields -# Also see #9494 - ->>> FormSet = inlineformset_factory(Repository, Revision, fields=('revision',), extra=1) ->>> data = { -... 'revision_set-TOTAL_FORMS': '1', -... 'revision_set-INITIAL_FORMS': '0', -... 'revision_set-MAX_NUM_FORMS': '', -... 'revision_set-0-repository': repository.pk, -... 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', -... 'revision_set-0-DELETE': '', -... } ->>> formset = FormSet(data, instance=repository) ->>> formset.is_valid() -False - -# Use of callable defaults (see bug #7975). - ->>> person = Person.objects.create(name='Ringo') ->>> FormSet = inlineformset_factory(Person, Membership, can_delete=False, extra=1) ->>> formset = FormSet(instance=person) - -# Django will render a hidden field for model fields that have a callable -# default. This is required to ensure the value is tested for change correctly -# when determine what extra forms have changed to save. - ->>> form = formset.forms[0] # this formset only has one form ->>> now = form.fields['date_joined'].initial() ->>> print form.as_p() -

        -

        - -# test for validation with callable defaults. Validations rely on hidden fields - ->>> data = { -... 'membership_set-TOTAL_FORMS': '1', -... 'membership_set-INITIAL_FORMS': '0', -... 'membership_set-MAX_NUM_FORMS': '', -... 'membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), -... 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), -... 'membership_set-0-karma': '', -... } ->>> formset = FormSet(data, instance=person) ->>> formset.is_valid() -True - -# now test for when the data changes - ->>> one_day_later = now + datetime.timedelta(days=1) ->>> filled_data = { -... 'membership_set-TOTAL_FORMS': '1', -... 'membership_set-INITIAL_FORMS': '0', -... 'membership_set-MAX_NUM_FORMS': '', -... 'membership_set-0-date_joined': unicode(one_day_later.strftime('%Y-%m-%d %H:%M:%S')), -... 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), -... 'membership_set-0-karma': '', -... } ->>> formset = FormSet(filled_data, instance=person) ->>> formset.is_valid() -False - -# now test with split datetime fields - ->>> class MembershipForm(forms.ModelForm): -... date_joined = forms.SplitDateTimeField(initial=now) -... class Meta: -... model = Membership -... def __init__(self, **kwargs): -... super(MembershipForm, self).__init__(**kwargs) -... self.fields['date_joined'].widget = forms.SplitDateTimeWidget() - ->>> FormSet = inlineformset_factory(Person, Membership, form=MembershipForm, can_delete=False, extra=1) ->>> data = { -... 'membership_set-TOTAL_FORMS': '1', -... 'membership_set-INITIAL_FORMS': '0', -... 'membership_set-MAX_NUM_FORMS': '', -... 'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')), -... 'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')), -... 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), -... 'membership_set-0-karma': '', -... } ->>> formset = FormSet(data, instance=person) ->>> formset.is_valid() -True - -# inlineformset_factory tests with fk having null=True. see #9462. -# create some data that will exbit the issue ->>> team = Team.objects.create(name=u"Red Vipers") ->>> Player(name="Timmy").save() ->>> Player(name="Bobby", team=team).save() - ->>> PlayerInlineFormSet = inlineformset_factory(Team, Player) ->>> formset = PlayerInlineFormSet() ->>> formset.get_queryset() -[] - ->>> formset = PlayerInlineFormSet(instance=team) ->>> formset.get_queryset() -[] - -# a formset for a Model that has a custom primary key that still needs to be -# added to the formset automatically ->>> FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"]) ->>> sorted(FormSet().forms[0].fields.keys()) -['restaurant', 'tacos_are_yummy'] - -# Prevent duplicates from within the same formset ->>> FormSet = modelformset_factory(Product, extra=2) ->>> data = { -... 'form-TOTAL_FORMS': 2, -... 'form-INITIAL_FORMS': 0, -... 'form-MAX_NUM_FORMS': '', -... 'form-0-slug': 'red_car', -... 'form-1-slug': 'red_car', -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -False ->>> formset._non_form_errors -[u'Please correct the duplicate data for slug.'] - ->>> FormSet = modelformset_factory(Price, extra=2) ->>> data = { -... 'form-TOTAL_FORMS': 2, -... 'form-INITIAL_FORMS': 0, -... 'form-MAX_NUM_FORMS': '', -... 'form-0-price': '25', -... 'form-0-quantity': '7', -... 'form-1-price': '25', -... 'form-1-quantity': '7', -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -False ->>> formset._non_form_errors -[u'Please correct the duplicate data for price and quantity, which must be unique.'] - -# Only the price field is specified, this should skip any unique checks since -# the unique_together is not fulfilled. This will fail with a KeyError if broken. ->>> FormSet = modelformset_factory(Price, fields=("price",), extra=2) ->>> data = { -... 'form-TOTAL_FORMS': '2', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... 'form-0-price': '24', -... 'form-1-price': '24', -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -True - ->>> FormSet = inlineformset_factory(Author, Book, extra=0) ->>> author = Author.objects.order_by('id')[0] ->>> book_ids = author.book_set.values_list('id', flat=True) ->>> data = { -... 'book_set-TOTAL_FORMS': '2', -... 'book_set-INITIAL_FORMS': '2', -... 'book_set-MAX_NUM_FORMS': '', -... -... 'book_set-0-title': 'The 2008 Election', -... 'book_set-0-author': str(author.id), -... 'book_set-0-id': str(book_ids[0]), -... -... 'book_set-1-title': 'The 2008 Election', -... 'book_set-1-author': str(author.id), -... 'book_set-1-id': str(book_ids[1]), -... } ->>> formset = FormSet(data=data, instance=author) ->>> formset.is_valid() -False ->>> formset._non_form_errors -[u'Please correct the duplicate data for title.'] ->>> formset.errors -[{}, {'__all__': u'Please correct the duplicate values below.'}] - ->>> FormSet = modelformset_factory(Post, extra=2) ->>> data = { -... 'form-TOTAL_FORMS': '2', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... -... 'form-0-title': 'blah', -... 'form-0-slug': 'Morning', -... 'form-0-subtitle': 'foo', -... 'form-0-posted': '2009-01-01', -... 'form-1-title': 'blah', -... 'form-1-slug': 'Morning in Prague', -... 'form-1-subtitle': 'rawr', -... 'form-1-posted': '2009-01-01' -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -False ->>> formset._non_form_errors -[u'Please correct the duplicate data for title which must be unique for the date in posted.'] ->>> formset.errors -[{}, {'__all__': u'Please correct the duplicate values below.'}] - ->>> data = { -... 'form-TOTAL_FORMS': '2', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... -... 'form-0-title': 'foo', -... 'form-0-slug': 'Morning in Prague', -... 'form-0-subtitle': 'foo', -... 'form-0-posted': '2009-01-01', -... 'form-1-title': 'blah', -... 'form-1-slug': 'Morning in Prague', -... 'form-1-subtitle': 'rawr', -... 'form-1-posted': '2009-08-02' -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -False ->>> formset._non_form_errors -[u'Please correct the duplicate data for slug which must be unique for the year in posted.'] - ->>> data = { -... 'form-TOTAL_FORMS': '2', -... 'form-INITIAL_FORMS': '0', -... 'form-MAX_NUM_FORMS': '', -... -... 'form-0-title': 'foo', -... 'form-0-slug': 'Morning in Prague', -... 'form-0-subtitle': 'rawr', -... 'form-0-posted': '2008-08-01', -... 'form-1-title': 'blah', -... 'form-1-slug': 'Prague', -... 'form-1-subtitle': 'rawr', -... 'form-1-posted': '2009-08-02' -... } ->>> formset = FormSet(data) ->>> formset.is_valid() -False ->>> formset._non_form_errors -[u'Please correct the duplicate data for subtitle which must be unique for the month in posted.'] -"""} diff --git a/tests/modeltests/model_formsets/tests.py b/tests/modeltests/model_formsets/tests.py index 62489bad3783..1664f7813b6c 100644 --- a/tests/modeltests/model_formsets/tests.py +++ b/tests/modeltests/model_formsets/tests.py @@ -1,6 +1,21 @@ +import datetime +import re +from datetime import date +from decimal import Decimal + +from django import forms +from django.conf import settings +from django.db import models, DEFAULT_DB_ALIAS +from django.forms.models import (_get_foreign_key, inlineformset_factory, + modelformset_factory, modelformset_factory) from django.test import TestCase -from django.forms.models import modelformset_factory -from modeltests.model_formsets.models import Poet, Poem + +from modeltests.model_formsets.models import ( + Author, BetterAuthor, Book, BookWithCustomPK, Editor, + BookWithOptionalAltEditor, AlternateBook, AuthorMeeting, CustomPrimaryKey, + Place, Owner, Location, OwnerProfile, Restaurant, Product, Price, + MexicanRestaurant, ClassyMexicanRestaurant, Repository, Revision, + Person, Membership, Team, Player, Poet, Poem, Post) class DeletionTests(TestCase): def test_deletion(self): @@ -16,7 +31,7 @@ def test_deletion(self): } formset = PoetFormSet(data, queryset=Poet.objects.all()) formset.save() - self.failUnless(formset.is_valid()) + self.assertTrue(formset.is_valid()) self.assertEqual(Poet.objects.count(), 0) def test_add_form_deletion_when_invalid(self): @@ -56,7 +71,7 @@ def test_change_form_deletion_when_invalid(self): 'form-TOTAL_FORMS': u'1', 'form-INITIAL_FORMS': u'1', 'form-MAX_NUM_FORMS': u'0', - 'form-0-id': u'1', + 'form-0-id': unicode(poet.id), 'form-0-name': u'x' * 1000, } formset = PoetFormSet(data, queryset=Poet.objects.all()) @@ -71,3 +86,1083 @@ def test_change_form_deletion_when_invalid(self): self.assertEqual(formset.is_valid(), True) formset.save() self.assertEqual(Poet.objects.count(), 0) + +class ModelFormsetTest(TestCase): + def test_simple_save(self): + qs = Author.objects.all() + AuthorFormSet = modelformset_factory(Author, extra=3) + + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '

        ') + self.assertEqual(formset.forms[1].as_p(), + '

        ') + self.assertEqual(formset.forms[2].as_p(), + '

        ') + + data = { + 'form-TOTAL_FORMS': '3', # the number of forms rendered + 'form-INITIAL_FORMS': '0', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-name': 'Charles Baudelaire', + 'form-1-name': 'Arthur Rimbaud', + 'form-2-name': '', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 2) + author1, author2 = saved + self.assertEqual(author1, Author.objects.get(name='Charles Baudelaire')) + self.assertEqual(author2, Author.objects.get(name='Arthur Rimbaud')) + + authors = list(Author.objects.order_by('name')) + self.assertEqual(authors, [author2, author1]) + + # Gah! We forgot Paul Verlaine. Let's create a formset to edit the + # existing authors with an extra form to add him. We *could* pass in a + # queryset to restrict the Author objects we edit, but in this case + # we'll use it to display them in alphabetical order by name. + + qs = Author.objects.order_by('name') + AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=False) + + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '

        ' % author2.id) + self.assertEqual(formset.forms[1].as_p(), + '

        ' % author1.id) + self.assertEqual(formset.forms[2].as_p(), + '

        ') + + data = { + 'form-TOTAL_FORMS': '3', # the number of forms rendered + 'form-INITIAL_FORMS': '2', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': str(author2.id), + 'form-0-name': 'Arthur Rimbaud', + 'form-1-id': str(author1.id), + 'form-1-name': 'Charles Baudelaire', + 'form-2-name': 'Paul Verlaine', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + # Only changed or new objects are returned from formset.save() + saved = formset.save() + self.assertEqual(len(saved), 1) + author3 = saved[0] + self.assertEqual(author3, Author.objects.get(name='Paul Verlaine')) + + authors = list(Author.objects.order_by('name')) + self.assertEqual(authors, [author2, author1, author3]) + + # This probably shouldn't happen, but it will. If an add form was + # marked for deletion, make sure we don't save that form. + + qs = Author.objects.order_by('name') + AuthorFormSet = modelformset_factory(Author, extra=1, can_delete=True) + + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 4) + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ' % author2.id) + self.assertEqual(formset.forms[1].as_p(), + '

        \n' + '

        ' % author1.id) + self.assertEqual(formset.forms[2].as_p(), + '

        \n' + '

        ' % author3.id) + self.assertEqual(formset.forms[3].as_p(), + '

        \n' + '

        ') + + data = { + 'form-TOTAL_FORMS': '4', # the number of forms rendered + 'form-INITIAL_FORMS': '3', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': str(author2.id), + 'form-0-name': 'Arthur Rimbaud', + 'form-1-id': str(author1.id), + 'form-1-name': 'Charles Baudelaire', + 'form-2-id': str(author3.id), + 'form-2-name': 'Paul Verlaine', + 'form-3-name': 'Walt Whitman', + 'form-3-DELETE': 'on', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + # No objects were changed or saved so nothing will come back. + + self.assertEqual(formset.save(), []) + + authors = list(Author.objects.order_by('name')) + self.assertEqual(authors, [author2, author1, author3]) + + # Let's edit a record to ensure save only returns that one record. + + data = { + 'form-TOTAL_FORMS': '4', # the number of forms rendered + 'form-INITIAL_FORMS': '3', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': str(author2.id), + 'form-0-name': 'Walt Whitman', + 'form-1-id': str(author1.id), + 'form-1-name': 'Charles Baudelaire', + 'form-2-id': str(author3.id), + 'form-2-name': 'Paul Verlaine', + 'form-3-name': '', + 'form-3-DELETE': '', + } + + formset = AuthorFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + # One record has changed. + + saved = formset.save() + self.assertEqual(len(saved), 1) + self.assertEqual(saved[0], Author.objects.get(name='Walt Whitman')) + + def test_commit_false(self): + # Test the behavior of commit=False and save_m2m + + author1 = Author.objects.create(name='Charles Baudelaire') + author2 = Author.objects.create(name='Paul Verlaine') + author3 = Author.objects.create(name='Walt Whitman') + + meeting = AuthorMeeting.objects.create(created=date.today()) + meeting.authors = Author.objects.all() + + # create an Author instance to add to the meeting. + + author4 = Author.objects.create(name=u'John Steinbeck') + + AuthorMeetingFormSet = modelformset_factory(AuthorMeeting, extra=1, can_delete=True) + data = { + 'form-TOTAL_FORMS': '2', # the number of forms rendered + 'form-INITIAL_FORMS': '1', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-id': str(meeting.id), + 'form-0-name': '2nd Tuesday of the Week Meeting', + 'form-0-authors': [author2.id, author1.id, author3.id, author4.id], + 'form-1-name': '', + 'form-1-authors': '', + 'form-1-DELETE': '', + } + formset = AuthorMeetingFormSet(data=data, queryset=AuthorMeeting.objects.all()) + self.assertTrue(formset.is_valid()) + + instances = formset.save(commit=False) + for instance in instances: + instance.created = date.today() + instance.save() + formset.save_m2m() + self.assertQuerysetEqual(instances[0].authors.all(), [ + '', + '', + '', + '', + ]) + + def test_max_num(self): + # Test the behavior of max_num with model formsets. It should allow + # all existing related objects/inlines for a given object to be + # displayed, but not allow the creation of new inlines beyond max_num. + + author1 = Author.objects.create(name='Charles Baudelaire') + author2 = Author.objects.create(name='Paul Verlaine') + author3 = Author.objects.create(name='Walt Whitman') + + qs = Author.objects.order_by('name') + + AuthorFormSet = modelformset_factory(Author, max_num=None, extra=3) + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 6) + self.assertEqual(len(formset.extra_forms), 3) + + AuthorFormSet = modelformset_factory(Author, max_num=4, extra=3) + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 4) + self.assertEqual(len(formset.extra_forms), 1) + + AuthorFormSet = modelformset_factory(Author, max_num=0, extra=3) + formset = AuthorFormSet(queryset=qs) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(len(formset.extra_forms), 0) + + AuthorFormSet = modelformset_factory(Author, max_num=None) + formset = AuthorFormSet(queryset=qs) + self.assertQuerysetEqual(formset.get_queryset(), [ + '', + '', + '', + ]) + + AuthorFormSet = modelformset_factory(Author, max_num=0) + formset = AuthorFormSet(queryset=qs) + self.assertQuerysetEqual(formset.get_queryset(), [ + '', + '', + '', + ]) + + AuthorFormSet = modelformset_factory(Author, max_num=4) + formset = AuthorFormSet(queryset=qs) + self.assertQuerysetEqual(formset.get_queryset(), [ + '', + '', + '', + ]) + + def test_custom_save_method(self): + class PoetForm(forms.ModelForm): + def save(self, commit=True): + # change the name to "Vladimir Mayakovsky" just to be a jerk. + author = super(PoetForm, self).save(commit=False) + author.name = u"Vladimir Mayakovsky" + if commit: + author.save() + return author + + PoetFormSet = modelformset_factory(Poet, form=PoetForm) + + data = { + 'form-TOTAL_FORMS': '3', # the number of forms rendered + 'form-INITIAL_FORMS': '0', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-name': 'Walt Whitman', + 'form-1-name': 'Charles Baudelaire', + 'form-2-name': '', + } + + qs = Poet.objects.all() + formset = PoetFormSet(data=data, queryset=qs) + self.assertTrue(formset.is_valid()) + + poets = formset.save() + self.assertEqual(len(poets), 2) + poet1, poet2 = poets + self.assertEqual(poet1.name, 'Vladimir Mayakovsky') + self.assertEqual(poet2.name, 'Vladimir Mayakovsky') + + def test_model_inheritance(self): + BetterAuthorFormSet = modelformset_factory(BetterAuthor) + formset = BetterAuthorFormSet() + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ') + + data = { + 'form-TOTAL_FORMS': '1', # the number of forms rendered + 'form-INITIAL_FORMS': '0', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-author_ptr': '', + 'form-0-name': 'Ernest Hemingway', + 'form-0-write_speed': '10', + } + + formset = BetterAuthorFormSet(data) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + author1, = saved + self.assertEqual(author1, BetterAuthor.objects.get(name='Ernest Hemingway')) + hemingway_id = BetterAuthor.objects.get(name="Ernest Hemingway").pk + + formset = BetterAuthorFormSet() + self.assertEqual(len(formset.forms), 2) + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ' % hemingway_id) + self.assertEqual(formset.forms[1].as_p(), + '

        \n' + '

        ') + + data = { + 'form-TOTAL_FORMS': '2', # the number of forms rendered + 'form-INITIAL_FORMS': '1', # the number of forms with initial data + 'form-MAX_NUM_FORMS': '', # the max number of forms + 'form-0-author_ptr': hemingway_id, + 'form-0-name': 'Ernest Hemingway', + 'form-0-write_speed': '10', + 'form-1-author_ptr': '', + 'form-1-name': '', + 'form-1-write_speed': '', + } + + formset = BetterAuthorFormSet(data) + self.assertTrue(formset.is_valid()) + self.assertEqual(formset.save(), []) + + def test_inline_formsets(self): + # We can also create a formset that is tied to a parent model. This is + # how the admin system's edit inline functionality works. + + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=3) + author = Author.objects.create(name='Charles Baudelaire') + + formset = AuthorBooksFormSet(instance=author) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '

        ' % author.id) + self.assertEqual(formset.forms[1].as_p(), + '

        ' % author.id) + self.assertEqual(formset.forms[2].as_p(), + '

        ' % author.id) + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-title': 'Les Fleurs du Mal', + 'book_set-1-title': '', + 'book_set-2-title': '', + } + + formset = AuthorBooksFormSet(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book1, = saved + self.assertEqual(book1, Book.objects.get(title='Les Fleurs du Mal')) + self.assertQuerysetEqual(author.book_set.all(), ['']) + + # Now that we've added a book to Charles Baudelaire, let's try adding + # another one. This time though, an edit form will be available for + # every existing book. + + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + author = Author.objects.get(name='Charles Baudelaire') + + formset = AuthorBooksFormSet(instance=author) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '

        ' % (author.id, book1.id)) + self.assertEqual(formset.forms[1].as_p(), + '

        ' % author.id) + self.assertEqual(formset.forms[2].as_p(), + '

        ' % author.id) + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': str(book1.id), + 'book_set-0-title': 'Les Fleurs du Mal', + 'book_set-1-title': 'Les Paradis Artificiels', + 'book_set-2-title': '', + } + + formset = AuthorBooksFormSet(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book2, = saved + self.assertEqual(book2, Book.objects.get(title='Les Paradis Artificiels')) + + # As you can see, 'Les Paradis Artificiels' is now a book belonging to + # Charles Baudelaire. + self.assertQuerysetEqual(author.book_set.order_by('title'), [ + '', + '', + ]) + + def test_inline_formsets_save_as_new(self): + # The save_as_new parameter lets you re-associate the data to a new + # instance. This is used in the admin for save_as functionality. + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + author = Author.objects.create(name='Charles Baudelaire') + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '2', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': '1', + 'book_set-0-title': 'Les Fleurs du Mal', + 'book_set-1-id': '2', + 'book_set-1-title': 'Les Paradis Artificiels', + 'book_set-2-title': '', + } + + formset = AuthorBooksFormSet(data, instance=Author(), save_as_new=True) + self.assertTrue(formset.is_valid()) + + new_author = Author.objects.create(name='Charles Baudelaire') + formset = AuthorBooksFormSet(data, instance=new_author, save_as_new=True) + saved = formset.save() + self.assertEqual(len(saved), 2) + book1, book2 = saved + self.assertEqual(book1.title, 'Les Fleurs du Mal') + self.assertEqual(book2.title, 'Les Paradis Artificiels') + + # Test using a custom prefix on an inline formset. + + formset = AuthorBooksFormSet(prefix="test") + self.assertEqual(len(formset.forms), 2) + self.assertEqual(formset.forms[0].as_p(), + '

        ') + self.assertEqual(formset.forms[1].as_p(), + '

        ') + + def test_inline_formsets_with_custom_pk(self): + # Test inline formsets where the inline-edited object has a custom + # primary key that is not the fk to the parent object. + + AuthorBooksFormSet2 = inlineformset_factory(Author, BookWithCustomPK, can_delete=False, extra=1) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + + formset = AuthorBooksFormSet2(instance=author) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ') + + data = { + 'bookwithcustompk_set-TOTAL_FORMS': '1', # the number of forms rendered + 'bookwithcustompk_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'bookwithcustompk_set-MAX_NUM_FORMS': '', # the max number of forms + 'bookwithcustompk_set-0-my_pk': '77777', + 'bookwithcustompk_set-0-title': 'Les Fleurs du Mal', + } + + formset = AuthorBooksFormSet2(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book1, = saved + self.assertEqual(book1.pk, 77777) + + book1 = author.bookwithcustompk_set.get() + self.assertEqual(book1.title, 'Les Fleurs du Mal') + + def test_inline_formsets_with_multi_table_inheritance(self): + # Test inline formsets where the inline-edited object uses multi-table + # inheritance, thus has a non AutoField yet auto-created primary key. + + AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + + formset = AuthorBooksFormSet3(instance=author) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ') + + data = { + 'alternatebook_set-TOTAL_FORMS': '1', # the number of forms rendered + 'alternatebook_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'alternatebook_set-MAX_NUM_FORMS': '', # the max number of forms + 'alternatebook_set-0-title': 'Flowers of Evil', + 'alternatebook_set-0-notes': 'English translation of Les Fleurs du Mal' + } + + formset = AuthorBooksFormSet3(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 1) + book1, = saved + self.assertEqual(book1.title, 'Flowers of Evil') + self.assertEqual(book1.notes, 'English translation of Les Fleurs du Mal') + + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.oracle': + def test_inline_formsets_with_nullable_unique_together(self): + # Test inline formsets where the inline-edited object has a + # unique_together constraint with a nullable member + + AuthorBooksFormSet4 = inlineformset_factory(Author, BookWithOptionalAltEditor, can_delete=False, extra=2) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + + data = { + 'bookwithoptionalalteditor_set-TOTAL_FORMS': '2', # the number of forms rendered + 'bookwithoptionalalteditor_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'bookwithoptionalalteditor_set-MAX_NUM_FORMS': '', # the max number of forms + 'bookwithoptionalalteditor_set-0-author': '1', + 'bookwithoptionalalteditor_set-0-title': 'Les Fleurs du Mal', + 'bookwithoptionalalteditor_set-1-author': '1', + 'bookwithoptionalalteditor_set-1-title': 'Les Fleurs du Mal', + } + formset = AuthorBooksFormSet4(data, instance=author) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 2) + book1, book2 = saved + self.assertEqual(book1.author_id, 1) + self.assertEqual(book1.title, 'Les Fleurs du Mal') + self.assertEqual(book2.author_id, 1) + self.assertEqual(book2.title, 'Les Fleurs du Mal') + + def test_inline_formsets_with_custom_save_method(self): + AuthorBooksFormSet = inlineformset_factory(Author, Book, can_delete=False, extra=2) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + book1 = Book.objects.create(pk=1, author=author, title='Les Paradis Artificiels') + book2 = Book.objects.create(pk=2, author=author, title='Les Fleurs du Mal') + book3 = Book.objects.create(pk=3, author=author, title='Flowers of Evil') + + class PoemForm(forms.ModelForm): + def save(self, commit=True): + # change the name to "Brooklyn Bridge" just to be a jerk. + poem = super(PoemForm, self).save(commit=False) + poem.name = u"Brooklyn Bridge" + if commit: + poem.save() + return poem + + PoemFormSet = inlineformset_factory(Poet, Poem, form=PoemForm) + + data = { + 'poem_set-TOTAL_FORMS': '3', # the number of forms rendered + 'poem_set-INITIAL_FORMS': '0', # the number of forms with initial data + 'poem_set-MAX_NUM_FORMS': '', # the max number of forms + 'poem_set-0-name': 'The Cloud in Trousers', + 'poem_set-1-name': 'I', + 'poem_set-2-name': '', + } + + poet = Poet.objects.create(name='Vladimir Mayakovsky') + formset = PoemFormSet(data=data, instance=poet) + self.assertTrue(formset.is_valid()) + + saved = formset.save() + self.assertEqual(len(saved), 2) + poem1, poem2 = saved + self.assertEqual(poem1.name, 'Brooklyn Bridge') + self.assertEqual(poem2.name, 'Brooklyn Bridge') + + # We can provide a custom queryset to our InlineFormSet: + + custom_qs = Book.objects.order_by('-title') + formset = AuthorBooksFormSet(instance=author, queryset=custom_qs) + self.assertEqual(len(formset.forms), 5) + self.assertEqual(formset.forms[0].as_p(), + '

        ') + self.assertEqual(formset.forms[1].as_p(), + '

        ') + self.assertEqual(formset.forms[2].as_p(), + '

        ') + self.assertEqual(formset.forms[3].as_p(), + '

        ') + self.assertEqual(formset.forms[4].as_p(), + '

        ') + + data = { + 'book_set-TOTAL_FORMS': '5', # the number of forms rendered + 'book_set-INITIAL_FORMS': '3', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': str(book1.id), + 'book_set-0-title': 'Les Paradis Artificiels', + 'book_set-1-id': str(book2.id), + 'book_set-1-title': 'Les Fleurs du Mal', + 'book_set-2-id': str(book3.id), + 'book_set-2-title': 'Flowers of Evil', + 'book_set-3-title': 'Revue des deux mondes', + 'book_set-4-title': '', + } + formset = AuthorBooksFormSet(data, instance=author, queryset=custom_qs) + self.assertTrue(formset.is_valid()) + + custom_qs = Book.objects.filter(title__startswith='F') + formset = AuthorBooksFormSet(instance=author, queryset=custom_qs) + self.assertEqual(formset.forms[0].as_p(), + '

        ') + self.assertEqual(formset.forms[1].as_p(), + '

        ') + self.assertEqual(formset.forms[2].as_p(), + '

        ') + + data = { + 'book_set-TOTAL_FORMS': '3', # the number of forms rendered + 'book_set-INITIAL_FORMS': '1', # the number of forms with initial data + 'book_set-MAX_NUM_FORMS': '', # the max number of forms + 'book_set-0-id': str(book3.id), + 'book_set-0-title': 'Flowers of Evil', + 'book_set-1-title': 'Revue des deux mondes', + 'book_set-2-title': '', + } + formset = AuthorBooksFormSet(data, instance=author, queryset=custom_qs) + self.assertTrue(formset.is_valid()) + + def test_custom_pk(self): + # We need to ensure that it is displayed + + CustomPrimaryKeyFormSet = modelformset_factory(CustomPrimaryKey) + formset = CustomPrimaryKeyFormSet() + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ') + + # Custom primary keys with ForeignKey, OneToOneField and AutoField ############ + + place = Place.objects.create(pk=1, name=u'Giordanos', city=u'Chicago') + + FormSet = inlineformset_factory(Place, Owner, extra=2, can_delete=False) + formset = FormSet(instance=place) + self.assertEqual(len(formset.forms), 2) + self.assertEqual(formset.forms[0].as_p(), + '

        ') + self.assertEqual(formset.forms[1].as_p(), + '

        ') + + data = { + 'owner_set-TOTAL_FORMS': '2', + 'owner_set-INITIAL_FORMS': '0', + 'owner_set-MAX_NUM_FORMS': '', + 'owner_set-0-auto_id': '', + 'owner_set-0-name': u'Joe Perry', + 'owner_set-1-auto_id': '', + 'owner_set-1-name': '', + } + formset = FormSet(data, instance=place) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + owner1, = saved + self.assertEqual(owner1.name, 'Joe Perry') + self.assertEqual(owner1.place.name, 'Giordanos') + + formset = FormSet(instance=place) + self.assertEqual(len(formset.forms), 3) + self.assertEqual(formset.forms[0].as_p(), + '

        ' + % owner1.auto_id) + self.assertEqual(formset.forms[1].as_p(), + '

        ') + self.assertEqual(formset.forms[2].as_p(), + '

        ') + + data = { + 'owner_set-TOTAL_FORMS': '3', + 'owner_set-INITIAL_FORMS': '1', + 'owner_set-MAX_NUM_FORMS': '', + 'owner_set-0-auto_id': unicode(owner1.auto_id), + 'owner_set-0-name': u'Joe Perry', + 'owner_set-1-auto_id': '', + 'owner_set-1-name': u'Jack Berry', + 'owner_set-2-auto_id': '', + 'owner_set-2-name': '', + } + formset = FormSet(data, instance=place) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + owner2, = saved + self.assertEqual(owner2.name, 'Jack Berry') + self.assertEqual(owner2.place.name, 'Giordanos') + + # Ensure a custom primary key that is a ForeignKey or OneToOneField get rendered for the user to choose. + + FormSet = modelformset_factory(OwnerProfile) + formset = FormSet() + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ' + % (owner1.auto_id, owner2.auto_id)) + + owner1 = Owner.objects.get(name=u'Joe Perry') + FormSet = inlineformset_factory(Owner, OwnerProfile, max_num=1, can_delete=False) + self.assertEqual(FormSet.max_num, 1) + + formset = FormSet(instance=owner1) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '

        ' + % owner1.auto_id) + + data = { + 'ownerprofile-TOTAL_FORMS': '1', + 'ownerprofile-INITIAL_FORMS': '0', + 'ownerprofile-MAX_NUM_FORMS': '1', + 'ownerprofile-0-owner': '', + 'ownerprofile-0-age': u'54', + } + formset = FormSet(data, instance=owner1) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + profile1, = saved + self.assertEqual(profile1.owner, owner1) + self.assertEqual(profile1.age, 54) + + formset = FormSet(instance=owner1) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '

        ' + % owner1.auto_id) + + data = { + 'ownerprofile-TOTAL_FORMS': '1', + 'ownerprofile-INITIAL_FORMS': '1', + 'ownerprofile-MAX_NUM_FORMS': '1', + 'ownerprofile-0-owner': unicode(owner1.auto_id), + 'ownerprofile-0-age': u'55', + } + formset = FormSet(data, instance=owner1) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + profile1, = saved + self.assertEqual(profile1.owner, owner1) + self.assertEqual(profile1.age, 55) + + def test_unique_true_enforces_max_num_one(self): + # ForeignKey with unique=True should enforce max_num=1 + + place = Place.objects.create(pk=1, name=u'Giordanos', city=u'Chicago') + + FormSet = inlineformset_factory(Place, Location, can_delete=False) + self.assertEqual(FormSet.max_num, 1) + + formset = FormSet(instance=place) + self.assertEqual(len(formset.forms), 1) + self.assertEqual(formset.forms[0].as_p(), + '

        \n' + '

        ') + + def test_foreign_keys_in_parents(self): + self.assertEqual(type(_get_foreign_key(Restaurant, Owner)), models.ForeignKey) + self.assertEqual(type(_get_foreign_key(MexicanRestaurant, Owner)), models.ForeignKey) + + def test_unique_validation(self): + FormSet = modelformset_factory(Product, extra=1) + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-slug': 'car-red', + } + formset = FormSet(data) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + product1, = saved + self.assertEqual(product1.slug, 'car-red') + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-slug': 'car-red', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'slug': [u'Product with this Slug already exists.']}]) + + def test_unique_together_validation(self): + FormSet = modelformset_factory(Price, extra=1) + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-price': u'12.00', + 'form-0-quantity': '1', + } + formset = FormSet(data) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + price1, = saved + self.assertEqual(price1.price, Decimal('12.00')) + self.assertEqual(price1.quantity, 1) + + data = { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-price': u'12.00', + 'form-0-quantity': '1', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'__all__': [u'Price with this Price and Quantity already exists.']}]) + + def test_unique_together_with_inlineformset_factory(self): + # Also see bug #8882. + + repository = Repository.objects.create(name=u'Test Repo') + FormSet = inlineformset_factory(Repository, Revision, extra=1) + data = { + 'revision_set-TOTAL_FORMS': '1', + 'revision_set-INITIAL_FORMS': '0', + 'revision_set-MAX_NUM_FORMS': '', + 'revision_set-0-repository': repository.pk, + 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', + 'revision_set-0-DELETE': '', + } + formset = FormSet(data, instance=repository) + self.assertTrue(formset.is_valid()) + saved = formset.save() + self.assertEqual(len(saved), 1) + revision1, = saved + self.assertEqual(revision1.repository, repository) + self.assertEqual(revision1.revision, '146239817507f148d448db38840db7c3cbf47c76') + + # attempt to save the same revision against against the same repo. + data = { + 'revision_set-TOTAL_FORMS': '1', + 'revision_set-INITIAL_FORMS': '0', + 'revision_set-MAX_NUM_FORMS': '', + 'revision_set-0-repository': repository.pk, + 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', + 'revision_set-0-DELETE': '', + } + formset = FormSet(data, instance=repository) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset.errors, [{'__all__': [u'Revision with this Repository and Revision already exists.']}]) + + # unique_together with inlineformset_factory with overridden form fields + # Also see #9494 + + FormSet = inlineformset_factory(Repository, Revision, fields=('revision',), extra=1) + data = { + 'revision_set-TOTAL_FORMS': '1', + 'revision_set-INITIAL_FORMS': '0', + 'revision_set-MAX_NUM_FORMS': '', + 'revision_set-0-repository': repository.pk, + 'revision_set-0-revision': '146239817507f148d448db38840db7c3cbf47c76', + 'revision_set-0-DELETE': '', + } + formset = FormSet(data, instance=repository) + self.assertFalse(formset.is_valid()) + + def test_callable_defaults(self): + # Use of callable defaults (see bug #7975). + + person = Person.objects.create(name='Ringo') + FormSet = inlineformset_factory(Person, Membership, can_delete=False, extra=1) + formset = FormSet(instance=person) + + # Django will render a hidden field for model fields that have a callable + # default. This is required to ensure the value is tested for change correctly + # when determine what extra forms have changed to save. + + self.assertEquals(len(formset.forms), 1) # this formset only has one form + form = formset.forms[0] + now = form.fields['date_joined'].initial() + result = form.as_p() + result = re.sub(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?', '__DATETIME__', result) + self.assertEqual(result, + '

        \n' + '

        ' + % person.id) + + # test for validation with callable defaults. Validations rely on hidden fields + + data = { + 'membership_set-TOTAL_FORMS': '1', + 'membership_set-INITIAL_FORMS': '0', + 'membership_set-MAX_NUM_FORMS': '', + 'membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'membership_set-0-karma': '', + } + formset = FormSet(data, instance=person) + self.assertTrue(formset.is_valid()) + + # now test for when the data changes + + one_day_later = now + datetime.timedelta(days=1) + filled_data = { + 'membership_set-TOTAL_FORMS': '1', + 'membership_set-INITIAL_FORMS': '0', + 'membership_set-MAX_NUM_FORMS': '', + 'membership_set-0-date_joined': unicode(one_day_later.strftime('%Y-%m-%d %H:%M:%S')), + 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'membership_set-0-karma': '', + } + formset = FormSet(filled_data, instance=person) + self.assertFalse(formset.is_valid()) + + # now test with split datetime fields + + class MembershipForm(forms.ModelForm): + date_joined = forms.SplitDateTimeField(initial=now) + class Meta: + model = Membership + def __init__(self, **kwargs): + super(MembershipForm, self).__init__(**kwargs) + self.fields['date_joined'].widget = forms.SplitDateTimeWidget() + + FormSet = inlineformset_factory(Person, Membership, form=MembershipForm, can_delete=False, extra=1) + data = { + 'membership_set-TOTAL_FORMS': '1', + 'membership_set-INITIAL_FORMS': '0', + 'membership_set-MAX_NUM_FORMS': '', + 'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')), + 'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')), + 'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')), + 'membership_set-0-karma': '', + } + formset = FormSet(data, instance=person) + self.assertTrue(formset.is_valid()) + + def test_inlineformset_factory_with_null_fk(self): + # inlineformset_factory tests with fk having null=True. see #9462. + # create some data that will exbit the issue + team = Team.objects.create(name=u"Red Vipers") + Player(name="Timmy").save() + Player(name="Bobby", team=team).save() + + PlayerInlineFormSet = inlineformset_factory(Team, Player) + formset = PlayerInlineFormSet() + self.assertQuerysetEqual(formset.get_queryset(), []) + + formset = PlayerInlineFormSet(instance=team) + players = formset.get_queryset() + self.assertEqual(len(players), 1) + player1, = players + self.assertEqual(player1.team, team) + self.assertEqual(player1.name, 'Bobby') + + def test_model_formset_with_custom_pk(self): + # a formset for a Model that has a custom primary key that still needs to be + # added to the formset automatically + FormSet = modelformset_factory(ClassyMexicanRestaurant, fields=["tacos_are_yummy"]) + self.assertEqual(sorted(FormSet().forms[0].fields.keys()), ['restaurant', 'tacos_are_yummy']) + + def test_prevent_duplicates_from_with_the_same_formset(self): + FormSet = modelformset_factory(Product, extra=2) + data = { + 'form-TOTAL_FORMS': 2, + 'form-INITIAL_FORMS': 0, + 'form-MAX_NUM_FORMS': '', + 'form-0-slug': 'red_car', + 'form-1-slug': 'red_car', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for slug.']) + + FormSet = modelformset_factory(Price, extra=2) + data = { + 'form-TOTAL_FORMS': 2, + 'form-INITIAL_FORMS': 0, + 'form-MAX_NUM_FORMS': '', + 'form-0-price': '25', + 'form-0-quantity': '7', + 'form-1-price': '25', + 'form-1-quantity': '7', + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for price and quantity, which must be unique.']) + + # Only the price field is specified, this should skip any unique checks since + # the unique_together is not fulfilled. This will fail with a KeyError if broken. + FormSet = modelformset_factory(Price, fields=("price",), extra=2) + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + 'form-0-price': '24', + 'form-1-price': '24', + } + formset = FormSet(data) + self.assertTrue(formset.is_valid()) + + FormSet = inlineformset_factory(Author, Book, extra=0) + author = Author.objects.create(pk=1, name='Charles Baudelaire') + book1 = Book.objects.create(pk=1, author=author, title='Les Paradis Artificiels') + book2 = Book.objects.create(pk=2, author=author, title='Les Fleurs du Mal') + book3 = Book.objects.create(pk=3, author=author, title='Flowers of Evil') + + book_ids = author.book_set.order_by('id').values_list('id', flat=True) + data = { + 'book_set-TOTAL_FORMS': '2', + 'book_set-INITIAL_FORMS': '2', + 'book_set-MAX_NUM_FORMS': '', + + 'book_set-0-title': 'The 2008 Election', + 'book_set-0-author': str(author.id), + 'book_set-0-id': str(book_ids[0]), + + 'book_set-1-title': 'The 2008 Election', + 'book_set-1-author': str(author.id), + 'book_set-1-id': str(book_ids[1]), + } + formset = FormSet(data=data, instance=author) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for title.']) + self.assertEqual(formset.errors, + [{}, {'__all__': [u'Please correct the duplicate values below.']}]) + + FormSet = modelformset_factory(Post, extra=2) + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + + 'form-0-title': 'blah', + 'form-0-slug': 'Morning', + 'form-0-subtitle': 'foo', + 'form-0-posted': '2009-01-01', + 'form-1-title': 'blah', + 'form-1-slug': 'Morning in Prague', + 'form-1-subtitle': 'rawr', + 'form-1-posted': '2009-01-01' + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for title which must be unique for the date in posted.']) + self.assertEqual(formset.errors, + [{}, {'__all__': [u'Please correct the duplicate values below.']}]) + + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + + 'form-0-title': 'foo', + 'form-0-slug': 'Morning in Prague', + 'form-0-subtitle': 'foo', + 'form-0-posted': '2009-01-01', + 'form-1-title': 'blah', + 'form-1-slug': 'Morning in Prague', + 'form-1-subtitle': 'rawr', + 'form-1-posted': '2009-08-02' + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for slug which must be unique for the year in posted.']) + + data = { + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '0', + 'form-MAX_NUM_FORMS': '', + + 'form-0-title': 'foo', + 'form-0-slug': 'Morning in Prague', + 'form-0-subtitle': 'rawr', + 'form-0-posted': '2008-08-01', + 'form-1-title': 'blah', + 'form-1-slug': 'Prague', + 'form-1-subtitle': 'rawr', + 'form-1-posted': '2009-08-02' + } + formset = FormSet(data) + self.assertFalse(formset.is_valid()) + self.assertEqual(formset._non_form_errors, + [u'Please correct the duplicate data for subtitle which must be unique for the month in posted.']) diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py index 95bf5abf9925..a0fed8a5ccd0 100644 --- a/tests/modeltests/model_inheritance/models.py +++ b/tests/modeltests/model_inheritance/models.py @@ -144,236 +144,10 @@ class Copy(NamedURL): def __unicode__(self): return self.content -__test__ = {'API_TESTS':""" -# The Student and Worker models both have 'name' and 'age' fields on them and -# inherit the __unicode__() method, just as with normal Python subclassing. -# This is useful if you want to factor out common information for programming -# purposes, but still completely independent separate models at the database -# level. - ->>> w = Worker(name='Fred', age=35, job='Quarry worker') ->>> w.save() ->>> w2 = Worker(name='Barney', age=34, job='Quarry worker') ->>> w2.save() ->>> s = Student(name='Pebbles', age=5, school_class='1B') ->>> s.save() ->>> unicode(w) -u'Worker Fred' ->>> unicode(s) -u'Student Pebbles' - -# The children inherit the Meta class of their parents (if they don't specify -# their own). ->>> Worker.objects.values('name') -[{'name': u'Barney'}, {'name': u'Fred'}] - -# Since Student does not subclass CommonInfo's Meta, it has the effect of -# completely overriding it. So ordering by name doesn't take place for Students. ->>> Student._meta.ordering -[] - -# However, the CommonInfo class cannot be used as a normal model (it doesn't -# exist as a model). ->>> CommonInfo.objects.all() -Traceback (most recent call last): - ... -AttributeError: type object 'CommonInfo' has no attribute 'objects' - -# A StudentWorker which does not exist is both a Student and Worker which does not exist. ->>> try: -... StudentWorker.objects.get(id=1) -... except Student.DoesNotExist: -... pass ->>> try: -... StudentWorker.objects.get(id=1) -... except Worker.DoesNotExist: -... pass - -# MultipleObjectsReturned is also inherited. ->>> sw1 = StudentWorker() ->>> sw1.name = 'Wilma' ->>> sw1.age = 35 ->>> sw1.save() ->>> sw2 = StudentWorker() ->>> sw2.name = 'Betty' ->>> sw2.age = 34 ->>> sw2.save() ->>> try: -... StudentWorker.objects.get(id__lt=10) -... except Student.MultipleObjectsReturned: -... pass -... except Worker.MultipleObjectsReturned: -... pass - -# Create a Post ->>> post = Post(title='Lorem Ipsum') ->>> post.save() - -# The Post model has distinct accessors for the Comment and Link models. ->>> post.attached_comment_set.create(content='Save $ on V1agr@', is_spam=True) - ->>> post.attached_link_set.create(content='The Web framework for perfectionists with deadlines.', url='http://www.djangoproject.com/') - - -# The Post model doesn't have an attribute called 'attached_%(class)s_set'. ->>> getattr(post, 'attached_%(class)s_set') -Traceback (most recent call last): - ... -AttributeError: 'Post' object has no attribute 'attached_%(class)s_set' - -# The Place/Restaurant/ItalianRestaurant models all exist as independent -# models. However, the subclasses also have transparent access to the fields of -# their ancestors. - -# Create a couple of Places. ->>> p1 = Place(name='Master Shakes', address='666 W. Jersey') ->>> p1.save() ->>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland') ->>> p2.save() - -Test constructor for Restaurant. ->>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton',serves_hot_dogs=True, serves_pizza=False, rating=2) ->>> r.save() - -# Test the constructor for ItalianRestaurant. ->>> c = Chef(name="Albert") ->>> c.save() ->>> ir = ItalianRestaurant(name='Ristorante Miron', address='1234 W. Ash', serves_hot_dogs=False, serves_pizza=False, serves_gnocchi=True, rating=4, chef=c) ->>> ir.save() ->>> ItalianRestaurant.objects.filter(address='1234 W. Ash') -[] - ->>> ir.address = '1234 W. Elm' ->>> ir.save() ->>> ItalianRestaurant.objects.filter(address='1234 W. Elm') -[] - -# Make sure Restaurant and ItalianRestaurant have the right fields in the right -# order. ->>> [f.name for f in Restaurant._meta.fields] -['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza', 'chef'] ->>> [f.name for f in ItalianRestaurant._meta.fields] -['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza', 'chef', 'restaurant_ptr', 'serves_gnocchi'] ->>> Restaurant._meta.ordering -['-rating'] - -# Even though p.supplier for a Place 'p' (a parent of a Supplier), a Restaurant -# object cannot access that reverse relation, since it's not part of the -# Place-Supplier Hierarchy. ->>> Place.objects.filter(supplier__name='foo') -[] ->>> Restaurant.objects.filter(supplier__name='foo') -Traceback (most recent call last): - ... -FieldError: Cannot resolve keyword 'supplier' into field. Choices are: address, chef, id, italianrestaurant, lot, name, place_ptr, provider, rating, serves_hot_dogs, serves_pizza - -# Parent fields can be used directly in filters on the child model. ->>> Restaurant.objects.filter(name='Demon Dogs') -[] ->>> ItalianRestaurant.objects.filter(address='1234 W. Elm') -[] - -# Filters against the parent model return objects of the parent's type. ->>> Place.objects.filter(name='Demon Dogs') -[] - -# Since the parent and child are linked by an automatically created -# OneToOneField, you can get from the parent to the child by using the child's -# name. ->>> place = Place.objects.get(name='Demon Dogs') ->>> place.restaurant - - ->>> Place.objects.get(name='Ristorante Miron').restaurant.italianrestaurant - ->>> Restaurant.objects.get(name='Ristorante Miron').italianrestaurant - - -# This won't work because the Demon Dogs restaurant is not an Italian -# restaurant. ->>> place.restaurant.italianrestaurant -Traceback (most recent call last): - ... -DoesNotExist: ItalianRestaurant matching query does not exist. - -# An ItalianRestaurant which does not exist is also a Place which does not exist. ->>> try: -... ItalianRestaurant.objects.get(name='The Noodle Void') -... except Place.DoesNotExist: -... pass - -# MultipleObjectsReturned is also inherited. ->>> try: -... Restaurant.objects.get(id__lt=10) -... except Place.MultipleObjectsReturned: -... pass - -# Related objects work just as they normally do. - ->>> s1 = Supplier(name="Joe's Chickens", address='123 Sesame St') ->>> s1.save() ->>> s1.customers = [r, ir] ->>> s2 = Supplier(name="Luigi's Pasta", address='456 Sesame St') ->>> s2.save() ->>> s2.customers = [ir] - -# This won't work because the Place we select is not a Restaurant (it's a -# Supplier). ->>> p = Place.objects.get(name="Joe's Chickens") ->>> p.restaurant -Traceback (most recent call last): - ... -DoesNotExist: Restaurant matching query does not exist. - -# But we can descend from p to the Supplier child, as expected. ->>> p.supplier - - ->>> ir.provider.order_by('-name') -[, ] - ->>> Restaurant.objects.filter(provider__name__contains="Chickens") -[, ] ->>> ItalianRestaurant.objects.filter(provider__name__contains="Chickens") -[] - ->>> park1 = ParkingLot(name='Main St', address='111 Main St', main_site=s1) ->>> park1.save() ->>> park2 = ParkingLot(name='Well Lit', address='124 Sesame St', main_site=ir) ->>> park2.save() - ->>> Restaurant.objects.get(lot__name='Well Lit') - - -# The update() command can update fields in parent and child classes at once -# (although it executed multiple SQL queries to do so). ->>> Restaurant.objects.filter(serves_hot_dogs=True, name__contains='D').update(name='Demon Puppies', serves_hot_dogs=False) -1 ->>> r1 = Restaurant.objects.get(pk=r.pk) ->>> r1.serves_hot_dogs == False -True ->>> r1.name -u'Demon Puppies' - -# The values() command also works on fields from parent models. ->>> d = {'rating': 4, 'name': u'Ristorante Miron'} ->>> list(ItalianRestaurant.objects.values('name', 'rating')) == [d] -True - -# select_related works with fields from the parent object as if they were a -# normal part of the model. ->>> from django import db ->>> from django.conf import settings ->>> settings.DEBUG = True ->>> db.reset_queries() ->>> ItalianRestaurant.objects.all()[0].chef - ->>> len(db.connection.queries) -2 ->>> ItalianRestaurant.objects.select_related('chef')[0].chef - ->>> len(db.connection.queries) -3 ->>> settings.DEBUG = False - -"""} +class Mixin(object): + def __init__(self): + self.other_attr = 1 + super(Mixin, self).__init__() + +class MixinModel(models.Model, Mixin): + pass diff --git a/tests/modeltests/model_inheritance/tests.py b/tests/modeltests/model_inheritance/tests.py new file mode 100644 index 000000000000..d4a24f5545c7 --- /dev/null +++ b/tests/modeltests/model_inheritance/tests.py @@ -0,0 +1,283 @@ +from operator import attrgetter + +from django.conf import settings +from django.core.exceptions import FieldError +from django.db import connection +from django.test import TestCase + +from models import (Chef, CommonInfo, ItalianRestaurant, ParkingLot, Place, + Post, Restaurant, Student, StudentWorker, Supplier, Worker, MixinModel) + + +class ModelInheritanceTests(TestCase): + def test_abstract(self): + # The Student and Worker models both have 'name' and 'age' fields on + # them and inherit the __unicode__() method, just as with normal Python + # subclassing. This is useful if you want to factor out common + # information for programming purposes, but still completely + # independent separate models at the database level. + w1 = Worker.objects.create(name="Fred", age=35, job="Quarry worker") + w2 = Worker.objects.create(name="Barney", age=34, job="Quarry worker") + + s = Student.objects.create(name="Pebbles", age=5, school_class="1B") + + self.assertEqual(unicode(w1), "Worker Fred") + self.assertEqual(unicode(s), "Student Pebbles") + + # The children inherit the Meta class of their parents (if they don't + # specify their own). + self.assertQuerysetEqual( + Worker.objects.values("name"), [ + {"name": "Barney"}, + {"name": "Fred"}, + ], + lambda o: o + ) + + # Since Student does not subclass CommonInfo's Meta, it has the effect + # of completely overriding it. So ordering by name doesn't take place + # for Students. + self.assertEqual(Student._meta.ordering, []) + + # However, the CommonInfo class cannot be used as a normal model (it + # doesn't exist as a model). + self.assertRaises(AttributeError, lambda: CommonInfo.objects.all()) + + # A StudentWorker which does not exist is both a Student and Worker + # which does not exist. + self.assertRaises(Student.DoesNotExist, + StudentWorker.objects.get, pk=12321321 + ) + self.assertRaises(Worker.DoesNotExist, + StudentWorker.objects.get, pk=12321321 + ) + + # MultipleObjectsReturned is also inherited. + # This is written out "long form", rather than using __init__/create() + # because of a bug with diamond inheritance (#10808) + sw1 = StudentWorker() + sw1.name = "Wilma" + sw1.age = 35 + sw1.save() + sw2 = StudentWorker() + sw2.name = "Betty" + sw2.age = 24 + sw2.save() + + self.assertRaises(Student.MultipleObjectsReturned, + StudentWorker.objects.get, pk__lt=sw2.pk + 100 + ) + self.assertRaises(Worker.MultipleObjectsReturned, + StudentWorker.objects.get, pk__lt=sw2.pk + 100 + ) + + def test_multiple_table(self): + post = Post.objects.create(title="Lorem Ipsum") + # The Post model has distinct accessors for the Comment and Link models. + post.attached_comment_set.create(content="Save $ on V1agr@", is_spam=True) + post.attached_link_set.create( + content="The Web framework for perfections with deadlines.", + url="http://www.djangoproject.com/" + ) + + # The Post model doesn't have an attribute called + # 'attached_%(class)s_set'. + self.assertRaises(AttributeError, + getattr, post, "attached_%(class)s_set" + ) + + # The Place/Restaurant/ItalianRestaurant models all exist as + # independent models. However, the subclasses also have transparent + # access to the fields of their ancestors. + # Create a couple of Places. + p1 = Place.objects.create(name="Master Shakes", address="666 W. Jersey") + p2 = Place.objects.create(name="Ace Harware", address="1013 N. Ashland") + + # Test constructor for Restaurant. + r = Restaurant.objects.create( + name="Demon Dogs", + address="944 W. Fullerton", + serves_hot_dogs=True, + serves_pizza=False, + rating=2 + ) + # Test the constructor for ItalianRestaurant. + c = Chef.objects.create(name="Albert") + ir = ItalianRestaurant.objects.create( + name="Ristorante Miron", + address="1234 W. Ash", + serves_hot_dogs=False, + serves_pizza=False, + serves_gnocchi=True, + rating=4, + chef=c + ) + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(address="1234 W. Ash"), [ + "Ristorante Miron", + ], + attrgetter("name") + ) + ir.address = "1234 W. Elm" + ir.save() + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(address="1234 W. Elm"), [ + "Ristorante Miron", + ], + attrgetter("name") + ) + + # Make sure Restaurant and ItalianRestaurant have the right fields in + # the right order. + self.assertEqual( + [f.name for f in Restaurant._meta.fields], + ["id", "name", "address", "place_ptr", "rating", "serves_hot_dogs", "serves_pizza", "chef"] + ) + self.assertEqual( + [f.name for f in ItalianRestaurant._meta.fields], + ["id", "name", "address", "place_ptr", "rating", "serves_hot_dogs", "serves_pizza", "chef", "restaurant_ptr", "serves_gnocchi"], + ) + self.assertEqual(Restaurant._meta.ordering, ["-rating"]) + + # Even though p.supplier for a Place 'p' (a parent of a Supplier), a + # Restaurant object cannot access that reverse relation, since it's not + # part of the Place-Supplier Hierarchy. + self.assertQuerysetEqual(Place.objects.filter(supplier__name="foo"), []) + self.assertRaises(FieldError, + Restaurant.objects.filter, supplier__name="foo" + ) + + # Parent fields can be used directly in filters on the child model. + self.assertQuerysetEqual( + Restaurant.objects.filter(name="Demon Dogs"), [ + "Demon Dogs", + ], + attrgetter("name") + ) + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(address="1234 W. Elm"), [ + "Ristorante Miron", + ], + attrgetter("name") + ) + + # Filters against the parent model return objects of the parent's type. + p = Place.objects.get(name="Demon Dogs") + self.assertTrue(type(p) is Place) + + # Since the parent and child are linked by an automatically created + # OneToOneField, you can get from the parent to the child by using the + # child's name. + self.assertEqual( + p.restaurant, Restaurant.objects.get(name="Demon Dogs") + ) + self.assertEqual( + Place.objects.get(name="Ristorante Miron").restaurant.italianrestaurant, + ItalianRestaurant.objects.get(name="Ristorante Miron") + ) + self.assertEqual( + Restaurant.objects.get(name="Ristorante Miron").italianrestaurant, + ItalianRestaurant.objects.get(name="Ristorante Miron") + ) + + # This won't work because the Demon Dogs restaurant is not an Italian + # restaurant. + self.assertRaises(ItalianRestaurant.DoesNotExist, + lambda: p.restaurant.italianrestaurant + ) + # An ItalianRestaurant which does not exist is also a Place which does + # not exist. + self.assertRaises(Place.DoesNotExist, + ItalianRestaurant.objects.get, name="The Noodle Void" + ) + # MultipleObjectsReturned is also inherited. + self.assertRaises(Place.MultipleObjectsReturned, + Restaurant.objects.get, id__lt=12321 + ) + + # Related objects work just as they normally do. + s1 = Supplier.objects.create(name="Joe's Chickens", address="123 Sesame St") + s1.customers = [r, ir] + s2 = Supplier.objects.create(name="Luigi's Pasta", address="456 Sesame St") + s2.customers = [ir] + + # This won't work because the Place we select is not a Restaurant (it's + # a Supplier). + p = Place.objects.get(name="Joe's Chickens") + self.assertRaises(Restaurant.DoesNotExist, + lambda: p.restaurant + ) + + self.assertEqual(p.supplier, s1) + self.assertQuerysetEqual( + ir.provider.order_by("-name"), [ + "Luigi's Pasta", + "Joe's Chickens" + ], + attrgetter("name") + ) + self.assertQuerysetEqual( + Restaurant.objects.filter(provider__name__contains="Chickens"), [ + "Ristorante Miron", + "Demon Dogs", + ], + attrgetter("name") + ) + self.assertQuerysetEqual( + ItalianRestaurant.objects.filter(provider__name__contains="Chickens"), [ + "Ristorante Miron", + ], + attrgetter("name"), + ) + + park1 = ParkingLot.objects.create( + name="Main St", address="111 Main St", main_site=s1 + ) + park2 = ParkingLot.objects.create( + name="Well Lit", address="124 Sesame St", main_site=ir + ) + + self.assertEqual( + Restaurant.objects.get(lot__name="Well Lit").name, + "Ristorante Miron" + ) + + # The update() command can update fields in parent and child classes at + # once (although it executed multiple SQL queries to do so). + rows = Restaurant.objects.filter( + serves_hot_dogs=True, name__contains="D" + ).update( + name="Demon Puppies", serves_hot_dogs=False + ) + self.assertEqual(rows, 1) + + r1 = Restaurant.objects.get(pk=r.pk) + self.assertFalse(r1.serves_hot_dogs) + self.assertEqual(r1.name, "Demon Puppies") + + # The values() command also works on fields from parent models. + self.assertQuerysetEqual( + ItalianRestaurant.objects.values("name", "rating"), [ + {"rating": 4, "name": "Ristorante Miron"} + ], + lambda o: o + ) + + # select_related works with fields from the parent object as if they + # were a normal part of the model. + old_DEBUG = settings.DEBUG + try: + settings.DEBUG = True + starting_queries = len(connection.queries) + ItalianRestaurant.objects.all()[0].chef + self.assertEqual(len(connection.queries) - starting_queries, 2) + + starting_queries = len(connection.queries) + ItalianRestaurant.objects.select_related("chef")[0].chef + self.assertEqual(len(connection.queries) - starting_queries, 1) + finally: + settings.DEBUG = old_DEBUG + + def test_mixin_init(self): + m = MixinModel() + self.assertEqual(m.other_attr, 1) diff --git a/tests/modeltests/model_package/tests.py b/tests/modeltests/model_package/tests.py index cf1785f70878..e63e2e63eae9 100644 --- a/tests/modeltests/model_package/tests.py +++ b/tests/modeltests/model_package/tests.py @@ -1,82 +1,72 @@ +from django.contrib.sites.models import Site from django.db import models +from django.test import TestCase + +from models.publication import Publication +from models.article import Article + class Advertisment(models.Model): customer = models.CharField(max_length=100) - publications = models.ManyToManyField("model_package.Publication", null=True, blank=True) + publications = models.ManyToManyField( + "model_package.Publication", null=True, blank=True + ) class Meta: app_label = 'model_package' -__test__ = {'API_TESTS': """ ->>> from models.publication import Publication ->>> from models.article import Article ->>> from django.contrib.auth.views import Site - ->>> p = Publication(title="FooBar") ->>> p.save() ->>> p - - ->>> from django.contrib.sites.models import Site ->>> current_site = Site.objects.get_current() ->>> current_site - - -# Regression for #12168: models split into subpackages still get M2M tables - ->>> a = Article(headline="a foo headline") ->>> a.save() ->>> a.publications.add(p) ->>> a.sites.add(current_site) - ->>> a = Article.objects.get(id=1) ->>> a - ->>> a.id -1 ->>> a.sites.count() -1 - -# Regression for #12245 - Models can exist in the test package, too - ->>> ad = Advertisment(customer="Lawrence Journal-World") ->>> ad.save() ->>> ad.publications.add(p) - ->>> ad = Advertisment.objects.get(id=1) ->>> ad - - ->>> ad.publications.count() -1 - -# Regression for #12386 - field names on the autogenerated intermediate class -# that are specified as dotted strings don't retain any path component for the -# field or column name - ->>> Article.publications.through._meta.fields[1].name -'article' - ->>> Article.publications.through._meta.fields[1].get_attname_column() -('article_id', 'article_id') - ->>> Article.publications.through._meta.fields[2].name -'publication' - ->>> Article.publications.through._meta.fields[2].get_attname_column() -('publication_id', 'publication_id') - -# The oracle backend truncates the name to 'model_package_article_publ233f'. ->>> Article._meta.get_field('publications').m2m_db_table() \\ -... in ('model_package_article_publications', 'model_package_article_publ233f') -True - ->>> Article._meta.get_field('publications').m2m_column_name() -'article_id' - ->>> Article._meta.get_field('publications').m2m_reverse_name() -'publication_id' - -"""} - +class ModelPackageTests(TestCase): + def test_model_packages(self): + p = Publication.objects.create(title="FooBar") + + current_site = Site.objects.get_current() + self.assertEqual(current_site.domain, "example.com") + + # Regression for #12168: models split into subpackages still get M2M + # tables + a = Article.objects.create(headline="a foo headline") + a.publications.add(p) + a.sites.add(current_site) + + a = Article.objects.get(id=a.pk) + self.assertEqual(a.id, a.pk) + self.assertEqual(a.sites.count(), 1) + + # Regression for #12245 - Models can exist in the test package, too + ad = Advertisment.objects.create(customer="Lawrence Journal-World") + ad.publications.add(p) + + ad = Advertisment.objects.get(id=ad.pk) + self.assertEqual(ad.publications.count(), 1) + + # Regression for #12386 - field names on the autogenerated intermediate + # class that are specified as dotted strings don't retain any path + # component for the field or column name + self.assertEqual( + Article.publications.through._meta.fields[1].name, 'article' + ) + self.assertEqual( + Article.publications.through._meta.fields[1].get_attname_column(), + ('article_id', 'article_id') + ) + self.assertEqual( + Article.publications.through._meta.fields[2].name, 'publication' + ) + self.assertEqual( + Article.publications.through._meta.fields[2].get_attname_column(), + ('publication_id', 'publication_id') + ) + + # The oracle backend truncates the name to 'model_package_article_publ233f'. + self.assertTrue( + Article._meta.get_field('publications').m2m_db_table() in ('model_package_article_publications', 'model_package_article_publ233f') + ) + + self.assertEqual( + Article._meta.get_field('publications').m2m_column_name(), 'article_id' + ) + self.assertEqual( + Article._meta.get_field('publications').m2m_reverse_name(), + 'publication_id' + ) diff --git a/tests/modeltests/mutually_referential/models.py b/tests/modeltests/mutually_referential/models.py index 2cbaa4b50b9b..db05cbc8a3f0 100644 --- a/tests/modeltests/mutually_referential/models.py +++ b/tests/modeltests/mutually_referential/models.py @@ -8,29 +8,12 @@ class Parent(Model): name = CharField(max_length=100) - + # Use a simple string for forward declarations. bestchild = ForeignKey("Child", null=True, related_name="favoured_by") class Child(Model): name = CharField(max_length=100) - + # You can also explicitally specify the related app. parent = ForeignKey("mutually_referential.Parent") - -__test__ = {'API_TESTS':""" -# Create a Parent ->>> q = Parent(name='Elizabeth') ->>> q.save() - -# Create some children ->>> c = q.child_set.create(name='Charles') ->>> e = q.child_set.create(name='Edward') - -# Set the best child ->>> q.bestchild = c ->>> q.save() - ->>> q.delete() - -"""} \ No newline at end of file diff --git a/tests/modeltests/mutually_referential/tests.py b/tests/modeltests/mutually_referential/tests.py new file mode 100644 index 000000000000..101d67cfa610 --- /dev/null +++ b/tests/modeltests/mutually_referential/tests.py @@ -0,0 +1,20 @@ +from django.test import TestCase +from models import Parent, Child + +class MutuallyReferentialTests(TestCase): + + def test_mutually_referential(self): + # Create a Parent + q = Parent(name='Elizabeth') + q.save() + + # Create some children + c = q.child_set.create(name='Charles') + e = q.child_set.create(name='Edward') + + # Set the best child + # No assertion require here; if basic assignment and + # deletion works, the test passes. + q.bestchild = c + q.save() + q.delete() diff --git a/tests/modeltests/one_to_one/models.py b/tests/modeltests/one_to_one/models.py index e25e33bcc0a5..f2637350cadd 100644 --- a/tests/modeltests/one_to_one/models.py +++ b/tests/modeltests/one_to_one/models.py @@ -45,150 +45,3 @@ class MultiModel(models.Model): def __unicode__(self): return u"Multimodel %s" % self.name - -__test__ = {'API_TESTS':""" -# Create a couple of Places. ->>> p1 = Place(name='Demon Dogs', address='944 W. Fullerton') ->>> p1.save() ->>> p2 = Place(name='Ace Hardware', address='1013 N. Ashland') ->>> p2.save() - -# Create a Restaurant. Pass the ID of the "parent" object as this object's ID. ->>> r = Restaurant(place=p1, serves_hot_dogs=True, serves_pizza=False) ->>> r.save() - -# A Restaurant can access its place. ->>> r.place - - -# A Place can access its restaurant, if available. ->>> p1.restaurant - - -# p2 doesn't have an associated restaurant. ->>> p2.restaurant -Traceback (most recent call last): - ... -DoesNotExist: Restaurant matching query does not exist. - -# Set the place using assignment notation. Because place is the primary key on -# Restaurant, the save will create a new restaurant ->>> r.place = p2 ->>> r.save() ->>> p2.restaurant - ->>> r.place - ->>> p2.id -2 - -# Set the place back again, using assignment in the reverse direction. ->>> p1.restaurant = r ->>> p1.restaurant - - ->>> r = Restaurant.objects.get(pk=1) ->>> r.place - - -# Restaurant.objects.all() just returns the Restaurants, not the Places. -# Note that there are two restaurants - Ace Hardware the Restaurant was created -# in the call to r.place = p2. ->>> Restaurant.objects.all() -[, ] - -# Place.objects.all() returns all Places, regardless of whether they have -# Restaurants. ->>> Place.objects.order_by('name') -[, ] - ->>> Restaurant.objects.get(place__id__exact=1) - ->>> Restaurant.objects.get(pk=1) - ->>> Restaurant.objects.get(place__exact=1) - ->>> Restaurant.objects.get(place__exact=p1) - ->>> Restaurant.objects.get(place=1) - ->>> Restaurant.objects.get(place=p1) - ->>> Restaurant.objects.get(place__pk=1) - ->>> Restaurant.objects.get(place__name__startswith="Demon") - - ->>> Place.objects.get(id__exact=1) - ->>> Place.objects.get(pk=1) - ->>> Place.objects.get(restaurant__place__exact=1) - ->>> Place.objects.get(restaurant__place__exact=p1) - ->>> Place.objects.get(restaurant__pk=1) - ->>> Place.objects.get(restaurant=1) - ->>> Place.objects.get(restaurant=r) - ->>> Place.objects.get(restaurant__exact=1) - ->>> Place.objects.get(restaurant__exact=r) - - -# Add a Waiter to the Restaurant. ->>> w = r.waiter_set.create(name='Joe') ->>> w.save() ->>> w - - -# Query the waiters ->>> Waiter.objects.filter(restaurant__place__pk=1) -[] ->>> Waiter.objects.filter(restaurant__place__exact=1) -[] ->>> Waiter.objects.filter(restaurant__place__exact=p1) -[] ->>> Waiter.objects.filter(restaurant__pk=1) -[] ->>> Waiter.objects.filter(id__exact=1) -[] ->>> Waiter.objects.filter(pk=1) -[] ->>> Waiter.objects.filter(restaurant=1) -[] ->>> Waiter.objects.filter(restaurant=r) -[] - -# Delete the restaurant; the waiter should also be removed ->>> r = Restaurant.objects.get(pk=1) ->>> r.delete() - -# One-to-one fields still work if you create your own primary key ->>> o1 = ManualPrimaryKey(primary_key="abc123", name="primary") ->>> o1.save() ->>> o2 = RelatedModel(link=o1, name="secondary") ->>> o2.save() - -# You can have multiple one-to-one fields on a model, too. ->>> x1 = MultiModel(link1=p1, link2=o1, name="x1") ->>> x1.save() ->>> o1.multimodel - - -# This will fail because each one-to-one field must be unique (and link2=o1 was -# used for x1, above). ->>> sid = transaction.savepoint() ->>> try: -... MultiModel(link1=p2, link2=o1, name="x1").save() -... except Exception, e: -... if isinstance(e, IntegrityError): -... print "Pass" -... else: -... print "Fail with %s" % type(e) -Pass ->>> transaction.savepoint_rollback(sid) - -"""} diff --git a/tests/modeltests/one_to_one/tests.py b/tests/modeltests/one_to_one/tests.py new file mode 100644 index 000000000000..c3e170445217 --- /dev/null +++ b/tests/modeltests/one_to_one/tests.py @@ -0,0 +1,119 @@ +from django.test import TestCase +from django.db import transaction, IntegrityError +from models import Place, Restaurant, Waiter, ManualPrimaryKey, RelatedModel, MultiModel + +class OneToOneTests(TestCase): + + def setUp(self): + self.p1 = Place(name='Demon Dogs', address='944 W. Fullerton') + self.p1.save() + self.p2 = Place(name='Ace Hardware', address='1013 N. Ashland') + self.p2.save() + self.r = Restaurant(place=self.p1, serves_hot_dogs=True, serves_pizza=False) + self.r.save() + + def test_getter(self): + # A Restaurant can access its place. + self.assertEqual(repr(self.r.place), '') + # A Place can access its restaurant, if available. + self.assertEqual(repr(self.p1.restaurant), '') + # p2 doesn't have an associated restaurant. + self.assertRaises(Restaurant.DoesNotExist, getattr, self.p2, 'restaurant') + + def test_setter(self): + # Set the place using assignment notation. Because place is the primary + # key on Restaurant, the save will create a new restaurant + self.r.place = self.p2 + self.r.save() + self.assertEqual(repr(self.p2.restaurant), '') + self.assertEqual(repr(self.r.place), '') + self.assertEqual(self.p2.pk, self.r.pk) + # Set the place back again, using assignment in the reverse direction. + self.p1.restaurant = self.r + self.assertEqual(repr(self.p1.restaurant), '') + r = Restaurant.objects.get(pk=self.p1.id) + self.assertEqual(repr(r.place), '') + + def test_manager_all(self): + # Restaurant.objects.all() just returns the Restaurants, not the Places. + self.assertQuerysetEqual(Restaurant.objects.all(), [ + '', + ]) + # Place.objects.all() returns all Places, regardless of whether they + # have Restaurants. + self.assertQuerysetEqual(Place.objects.order_by('name'), [ + '', + '', + ]) + + def test_manager_get(self): + def assert_get_restaurant(**params): + self.assertEqual(repr(Restaurant.objects.get(**params)), + '') + assert_get_restaurant(place__id__exact=self.p1.pk) + assert_get_restaurant(place__id=self.p1.pk) + assert_get_restaurant(place__exact=self.p1.pk) + assert_get_restaurant(place__exact=self.p1) + assert_get_restaurant(place=self.p1.pk) + assert_get_restaurant(place=self.p1) + assert_get_restaurant(pk=self.p1.pk) + assert_get_restaurant(place__pk__exact=self.p1.pk) + assert_get_restaurant(place__pk=self.p1.pk) + assert_get_restaurant(place__name__startswith="Demon") + + def assert_get_place(**params): + self.assertEqual(repr(Place.objects.get(**params)), + '') + assert_get_place(restaurant__place__exact=self.p1.pk) + assert_get_place(restaurant__place__exact=self.p1) + assert_get_place(restaurant__place__pk=self.p1.pk) + assert_get_place(restaurant__exact=self.p1.pk) + assert_get_place(restaurant__exact=self.r) + assert_get_place(restaurant__pk=self.p1.pk) + assert_get_place(restaurant=self.p1.pk) + assert_get_place(restaurant=self.r) + assert_get_place(id__exact=self.p1.pk) + assert_get_place(pk=self.p1.pk) + + def test_foreign_key(self): + # Add a Waiter to the Restaurant. + w = self.r.waiter_set.create(name='Joe') + w.save() + self.assertEqual(repr(w), '') + # Query the waiters + def assert_filter_waiters(**params): + self.assertQuerysetEqual(Waiter.objects.filter(**params), [ + '' + ]) + assert_filter_waiters(restaurant__place__exact=self.p1.pk) + assert_filter_waiters(restaurant__place__exact=self.p1) + assert_filter_waiters(restaurant__place__pk=self.p1.pk) + assert_filter_waiters(restaurant__exact=self.p1.pk) + assert_filter_waiters(restaurant__exact=self.p1) + assert_filter_waiters(restaurant__pk=self.p1.pk) + assert_filter_waiters(restaurant=self.p1.pk) + assert_filter_waiters(restaurant=self.r) + assert_filter_waiters(id__exact=self.p1.pk) + assert_filter_waiters(pk=self.p1.pk) + # Delete the restaurant; the waiter should also be removed + r = Restaurant.objects.get(pk=self.p1.pk) + r.delete() + self.assertEqual(Waiter.objects.count(), 0) + + def test_multiple_o2o(self): + # One-to-one fields still work if you create your own primary key + o1 = ManualPrimaryKey(primary_key="abc123", name="primary") + o1.save() + o2 = RelatedModel(link=o1, name="secondary") + o2.save() + + # You can have multiple one-to-one fields on a model, too. + x1 = MultiModel(link1=self.p1, link2=o1, name="x1") + x1.save() + self.assertEqual(repr(o1.multimodel), '') + # This will fail because each one-to-one field must be unique (and + # link2=o1 was used for x1, above). + sid = transaction.savepoint() + mm = MultiModel(link1=self.p2, link2=o1, name="x1") + self.assertRaises(IntegrityError, mm.save) + transaction.savepoint_rollback(sid) diff --git a/tests/modeltests/or_lookups/models.py b/tests/modeltests/or_lookups/models.py index 1179c6d2dc57..7f14ba50eb6e 100644 --- a/tests/modeltests/or_lookups/models.py +++ b/tests/modeltests/or_lookups/models.py @@ -20,112 +20,3 @@ class Meta: def __unicode__(self): return self.headline - -__test__ = {'API_TESTS':""" ->>> from datetime import datetime ->>> from django.db.models import Q - ->>> a1 = Article(headline='Hello', pub_date=datetime(2005, 11, 27)) ->>> a1.save() - ->>> a2 = Article(headline='Goodbye', pub_date=datetime(2005, 11, 28)) ->>> a2.save() - ->>> a3 = Article(headline='Hello and goodbye', pub_date=datetime(2005, 11, 29)) ->>> a3.save() - ->>> Article.objects.filter(headline__startswith='Hello') | Article.objects.filter(headline__startswith='Goodbye') -[, , ] - ->>> Article.objects.filter(Q(headline__startswith='Hello') | Q(headline__startswith='Goodbye')) -[, , ] - ->>> Article.objects.filter(Q(headline__startswith='Hello') & Q(headline__startswith='Goodbye')) -[] - -# You can shorten this syntax with code like the following, -# which is especially useful if building the query in stages: ->>> articles = Article.objects.all() ->>> articles.filter(headline__startswith='Hello') & articles.filter(headline__startswith='Goodbye') -[] - ->>> articles.filter(headline__startswith='Hello') & articles.filter(headline__contains='bye') -[] - ->>> Article.objects.filter(Q(headline__contains='bye'), headline__startswith='Hello') -[] - ->>> Article.objects.filter(headline__contains='Hello') | Article.objects.filter(headline__contains='bye') -[, , ] - ->>> Article.objects.filter(headline__iexact='Hello') | Article.objects.filter(headline__contains='ood') -[, , ] - ->>> Article.objects.filter(Q(pk=1) | Q(pk=2)) -[, ] - ->>> Article.objects.filter(Q(pk=1) | Q(pk=2) | Q(pk=3)) -[, , ] - -# You could also use "in" to accomplish the same as above. ->>> Article.objects.filter(pk__in=[1,2,3]) -[, , ] ->>> Article.objects.filter(pk__in=(1,2,3)) -[, , ] - ->>> Article.objects.filter(pk__in=[1,2,3,4]) -[, , ] - -# Passing "in" an empty list returns no results ... ->>> Article.objects.filter(pk__in=[]) -[] - -# ... but can return results if we OR it with another query. ->>> Article.objects.filter(Q(pk__in=[]) | Q(headline__icontains='goodbye')) -[, ] - -# Q arg objects are ANDed ->>> Article.objects.filter(Q(headline__startswith='Hello'), Q(headline__contains='bye')) -[] - -# Q arg AND order is irrelevant ->>> Article.objects.filter(Q(headline__contains='bye'), headline__startswith='Hello') -[] - -# Q objects can be negated ->>> Article.objects.filter(Q(pk=1) | ~Q(pk=2)) -[, ] ->>> Article.objects.filter(~Q(pk=1) & ~Q(pk=2)) -[] - -# This allows for more complex queries than filter() and exclude() alone would -# allow ->>> Article.objects.filter(Q(pk=1) & (~Q(pk=2) | Q(pk=3))) -[] - -# Try some arg queries with operations other than filter. ->>> Article.objects.get(Q(headline__startswith='Hello'), Q(headline__contains='bye')) - - ->>> Article.objects.filter(Q(headline__startswith='Hello') | Q(headline__contains='bye')).count() -3 - ->>> dicts = list(Article.objects.filter(Q(headline__startswith='Hello'), Q(headline__contains='bye')).values()) ->>> [sorted(d.items()) for d in dicts] -[[('headline', u'Hello and goodbye'), ('id', 3), ('pub_date', datetime.datetime(2005, 11, 29, 0, 0))]] - ->>> Article.objects.filter(Q(headline__startswith='Hello')).in_bulk([1,2]) -{1: } - -# Demonstrating exclude with a Q object ->>> Article.objects.exclude(Q(headline__startswith='Hello')) -[] - -# The 'complex_filter' method supports framework features such as -# 'limit_choices_to' which normally take a single dictionary of lookup arguments -# but need to support arbitrary queries via Q objects too. ->>> Article.objects.complex_filter({'pk': 1}) -[] ->>> Article.objects.complex_filter(Q(pk=1) | Q(pk=2)) -[, ] -"""} diff --git a/tests/modeltests/or_lookups/tests.py b/tests/modeltests/or_lookups/tests.py new file mode 100644 index 000000000000..ad218cd0b218 --- /dev/null +++ b/tests/modeltests/or_lookups/tests.py @@ -0,0 +1,232 @@ +from datetime import datetime +from operator import attrgetter + +from django.db.models import Q +from django.test import TestCase + +from models import Article + + +class OrLookupsTests(TestCase): + + def setUp(self): + self.a1 = Article.objects.create( + headline='Hello', pub_date=datetime(2005, 11, 27) + ).pk + self.a2 = Article.objects.create( + headline='Goodbye', pub_date=datetime(2005, 11, 28) + ).pk + self.a3 = Article.objects.create( + headline='Hello and goodbye', pub_date=datetime(2005, 11, 29) + ).pk + + def test_filter_or(self): + self.assertQuerysetEqual( + Article.objects.filter(headline__startswith='Hello') | Article.objects.filter(headline__startswith='Goodbye'), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(headline__contains='Hello') | Article.objects.filter(headline__contains='bye'), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(headline__iexact='Hello') | Article.objects.filter(headline__contains='ood'), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello') | Q(headline__startswith='Goodbye')), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + + def test_stages(self): + # You can shorten this syntax with code like the following, which is + # especially useful if building the query in stages: + articles = Article.objects.all() + self.assertQuerysetEqual( + articles.filter(headline__startswith='Hello') & articles.filter(headline__startswith='Goodbye'), + [] + ) + self.assertQuerysetEqual( + articles.filter(headline__startswith='Hello') & articles.filter(headline__contains='bye'), [ + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + def test_pk_q(self): + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) | Q(pk=self.a2)), [ + 'Hello', + 'Goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) | Q(pk=self.a2) | Q(pk=self.a3)), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + def test_pk_in(self): + self.assertQuerysetEqual( + Article.objects.filter(pk__in=[self.a1, self.a2, self.a3]), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.filter(pk__in=(self.a1, self.a2, self.a3)), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.filter(pk__in=[self.a1, self.a2, self.a3, 40000]), [ + 'Hello', + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + def test_q_negated(self): + # Q objects can be negated + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) | ~Q(pk=self.a2)), [ + 'Hello', + 'Hello and goodbye' + ], + attrgetter("headline") + ) + + self.assertQuerysetEqual( + Article.objects.filter(~Q(pk=self.a1) & ~Q(pk=self.a2)), [ + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + # This allows for more complex queries than filter() and exclude() + # alone would allow + self.assertQuerysetEqual( + Article.objects.filter(Q(pk=self.a1) & (~Q(pk=self.a2) | Q(pk=self.a3))), [ + 'Hello' + ], + attrgetter("headline"), + ) + + def test_complex_filter(self): + # The 'complex_filter' method supports framework features such as + # 'limit_choices_to' which normally take a single dictionary of lookup + # arguments but need to support arbitrary queries via Q objects too. + self.assertQuerysetEqual( + Article.objects.complex_filter({'pk': self.a1}), [ + 'Hello' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.complex_filter(Q(pk=self.a1) | Q(pk=self.a2)), [ + 'Hello', + 'Goodbye' + ], + attrgetter("headline"), + ) + + def test_empty_in(self): + # Passing "in" an empty list returns no results ... + self.assertQuerysetEqual( + Article.objects.filter(pk__in=[]), + [] + ) + # ... but can return results if we OR it with another query. + self.assertQuerysetEqual( + Article.objects.filter(Q(pk__in=[]) | Q(headline__icontains='goodbye')), [ + 'Goodbye', + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + def test_q_and(self): + # Q arg objects are ANDed + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello'), Q(headline__contains='bye')), [ + 'Hello and goodbye' + ], + attrgetter("headline") + ) + # Q arg AND order is irrelevant + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__contains='bye'), headline__startswith='Hello'), [ + 'Hello and goodbye' + ], + attrgetter("headline"), + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello') & Q(headline__startswith='Goodbye')), + [] + ) + + def test_q_exclude(self): + self.assertQuerysetEqual( + Article.objects.exclude(Q(headline__startswith='Hello')), [ + 'Goodbye' + ], + attrgetter("headline") + ) + + def test_other_arg_queries(self): + # Try some arg queries with operations other than filter. + self.assertEqual( + Article.objects.get(Q(headline__startswith='Hello'), Q(headline__contains='bye')).headline, + 'Hello and goodbye' + ) + + self.assertEqual( + Article.objects.filter(Q(headline__startswith='Hello') | Q(headline__contains='bye')).count(), + 3 + ) + + self.assertQuerysetEqual( + Article.objects.filter(Q(headline__startswith='Hello'), Q(headline__contains='bye')).values(), [ + {"headline": "Hello and goodbye", "id": self.a3, "pub_date": datetime(2005, 11, 29)}, + ], + lambda o: o, + ) + + self.assertEqual( + Article.objects.filter(Q(headline__startswith='Hello')).in_bulk([self.a1, self.a2]), + {self.a1: Article.objects.get(pk=self.a1)} + ) diff --git a/tests/modeltests/order_with_respect_to/models.py b/tests/modeltests/order_with_respect_to/models.py index 99c9f13e2efb..59f01d4cd146 100644 --- a/tests/modeltests/order_with_respect_to/models.py +++ b/tests/modeltests/order_with_respect_to/models.py @@ -4,6 +4,7 @@ from django.db import models + class Question(models.Model): text = models.CharField(max_length=200) @@ -17,62 +18,12 @@ class Meta: def __unicode__(self): return unicode(self.text) -__test__ = {'API_TESTS': """ ->>> q1 = Question(text="Which Beatle starts with the letter 'R'?") ->>> q1.save() ->>> q2 = Question(text="What is your name?") ->>> q2.save() ->>> Answer(text="John", question=q1).save() ->>> Answer(text="Jonno",question=q2).save() ->>> Answer(text="Paul", question=q1).save() ->>> Answer(text="Paulo", question=q2).save() ->>> Answer(text="George", question=q1).save() ->>> Answer(text="Ringo", question=q1).save() - -The answers will always be ordered in the order they were inserted. - ->>> q1.answer_set.all() -[, , , ] - -We can retrieve the answers related to a particular object, in the order -they were created, once we have a particular object. - ->>> a1 = Answer.objects.filter(question=q1)[0] ->>> a1 - ->>> a2 = a1.get_next_in_order() ->>> a2 - ->>> a4 = list(Answer.objects.filter(question=q1))[-1] ->>> a4 - ->>> a4.get_previous_in_order() - - -Determining (and setting) the ordering for a particular item is also possible. - ->>> id_list = [o.pk for o in q1.answer_set.all()] ->>> a2.question.get_answer_order() == id_list -True +class Post(models.Model): + title = models.CharField(max_length=200) + parent = models.ForeignKey("self", related_name="children", null=True) ->>> a5 = Answer(text="Number five", question=q1) ->>> a5.save() - -It doesn't matter which answer we use to check the order, it will always be the same. - ->>> a2.question.get_answer_order() == a5.question.get_answer_order() -True - -The ordering can be altered: - ->>> id_list = [o.pk for o in q1.answer_set.all()] ->>> x = id_list.pop() ->>> id_list.insert(-1, x) ->>> a5.question.get_answer_order() == id_list -False ->>> a5.question.set_answer_order(id_list) ->>> q1.answer_set.all() -[, , , , ] + class Meta: + order_with_respect_to = "parent" -""" -} + def __unicode__(self): + return self.title diff --git a/tests/modeltests/order_with_respect_to/tests.py b/tests/modeltests/order_with_respect_to/tests.py new file mode 100644 index 000000000000..328d968fd4ba --- /dev/null +++ b/tests/modeltests/order_with_respect_to/tests.py @@ -0,0 +1,71 @@ +from operator import attrgetter + +from django.test import TestCase + +from models import Post, Question, Answer + + +class OrderWithRespectToTests(TestCase): + def test_basic(self): + q1 = Question.objects.create(text="Which Beatle starts with the letter 'R'?") + q2 = Question.objects.create(text="What is your name?") + + Answer.objects.create(text="John", question=q1) + Answer.objects.create(text="Jonno", question=q2) + Answer.objects.create(text="Paul", question=q1) + Answer.objects.create(text="Paulo", question=q2) + Answer.objects.create(text="George", question=q1) + Answer.objects.create(text="Ringo", question=q1) + + # The answers will always be ordered in the order they were inserted. + self.assertQuerysetEqual( + q1.answer_set.all(), [ + "John", "Paul", "George", "Ringo", + ], + attrgetter("text"), + ) + + # We can retrieve the answers related to a particular object, in the + # order they were created, once we have a particular object. + a1 = Answer.objects.filter(question=q1)[0] + self.assertEqual(a1.text, "John") + a2 = a1.get_next_in_order() + self.assertEqual(a2.text, "Paul") + a4 = list(Answer.objects.filter(question=q1))[-1] + self.assertEqual(a4.text, "Ringo") + self.assertEqual(a4.get_previous_in_order().text, "George") + + # Determining (and setting) the ordering for a particular item is also + # possible. + id_list = [o.pk for o in q1.answer_set.all()] + self.assertEqual(a2.question.get_answer_order(), id_list) + + a5 = Answer.objects.create(text="Number five", question=q1) + + # It doesn't matter which answer we use to check the order, it will + # always be the same. + self.assertEqual( + a2.question.get_answer_order(), a5.question.get_answer_order() + ) + + # The ordering can be altered: + id_list = [o.pk for o in q1.answer_set.all()] + x = id_list.pop() + id_list.insert(-1, x) + self.assertNotEqual(a5.question.get_answer_order(), id_list) + a5.question.set_answer_order(id_list) + self.assertQuerysetEqual( + q1.answer_set.all(), [ + "John", "Paul", "George", "Number five", "Ringo" + ], + attrgetter("text") + ) + + def test_recursive_ordering(self): + p1 = Post.objects.create(title='1') + p2 = Post.objects.create(title='2') + p1_1 = Post.objects.create(title="1.1", parent=p1) + p1_2 = Post.objects.create(title="1.2", parent=p1) + p2_1 = Post.objects.create(title="2.1", parent=p2) + p1_3 = Post.objects.create(title="1.3", parent=p1) + self.assertEqual(p1.get_post_order(), [p1_1.pk, p1_2.pk, p1_3.pk]) diff --git a/tests/modeltests/ordering/models.py b/tests/modeltests/ordering/models.py index a53d93c33002..25d3c2c90f1f 100644 --- a/tests/modeltests/ordering/models.py +++ b/tests/modeltests/ordering/models.py @@ -15,6 +15,7 @@ from django.db import models + class Article(models.Model): headline = models.CharField(max_length=100) pub_date = models.DateTimeField() @@ -23,67 +24,3 @@ class Meta: def __unicode__(self): return self.headline - -__test__ = {'API_TESTS':""" -# Create a couple of Articles. ->>> from datetime import datetime ->>> a1 = Article(headline='Article 1', pub_date=datetime(2005, 7, 26)) ->>> a1.save() ->>> a2 = Article(headline='Article 2', pub_date=datetime(2005, 7, 27)) ->>> a2.save() ->>> a3 = Article(headline='Article 3', pub_date=datetime(2005, 7, 27)) ->>> a3.save() ->>> a4 = Article(headline='Article 4', pub_date=datetime(2005, 7, 28)) ->>> a4.save() - -# By default, Article.objects.all() orders by pub_date descending, then -# headline ascending. ->>> Article.objects.all() -[, , , ] - -# Override ordering with order_by, which is in the same format as the ordering -# attribute in models. ->>> Article.objects.order_by('headline') -[, , , ] ->>> Article.objects.order_by('pub_date', '-headline') -[, , , ] - -# Only the last order_by has any effect (since they each override any previous -# ordering). ->>> Article.objects.order_by('id') -[, , , ] ->>> Article.objects.order_by('id').order_by('-headline') -[, , , ] - -# Use the 'stop' part of slicing notation to limit the results. ->>> Article.objects.order_by('headline')[:2] -[, ] - -# Use the 'stop' and 'start' parts of slicing notation to offset the result list. ->>> Article.objects.order_by('headline')[1:3] -[, ] - -# Getting a single item should work too: ->>> Article.objects.all()[0] - - -# Use '?' to order randomly. (We're using [...] in the output to indicate we -# don't know what order the output will be in. ->>> Article.objects.order_by('?') -[...] - -# Ordering can be reversed using the reverse() method on a queryset. This -# allows you to extract things like "the last two items" (reverse and then -# take the first two). ->>> Article.objects.all().reverse()[:2] -[, ] - -# Ordering can be based on fields included from an 'extra' clause ->>> Article.objects.extra(select={'foo': 'pub_date'}, order_by=['foo', 'headline']) -[, , , ] - -# If the extra clause uses an SQL keyword for a name, it will be protected by quoting. ->>> Article.objects.extra(select={'order': 'pub_date'}, order_by=['order', 'headline']) -[, , , ] - -"""} diff --git a/tests/modeltests/ordering/tests.py b/tests/modeltests/ordering/tests.py new file mode 100644 index 000000000000..77862c528cb0 --- /dev/null +++ b/tests/modeltests/ordering/tests.py @@ -0,0 +1,137 @@ +from datetime import datetime +from operator import attrgetter + +from django.test import TestCase + +from models import Article + + +class OrderingTests(TestCase): + def test_basic(self): + a1 = Article.objects.create( + headline="Article 1", pub_date=datetime(2005, 7, 26) + ) + a2 = Article.objects.create( + headline="Article 2", pub_date=datetime(2005, 7, 27) + ) + a3 = Article.objects.create( + headline="Article 3", pub_date=datetime(2005, 7, 27) + ) + a4 = Article.objects.create( + headline="Article 4", pub_date=datetime(2005, 7, 28) + ) + + # By default, Article.objects.all() orders by pub_date descending, then + # headline ascending. + self.assertQuerysetEqual( + Article.objects.all(), [ + "Article 4", + "Article 2", + "Article 3", + "Article 1", + ], + attrgetter("headline") + ) + + # Override ordering with order_by, which is in the same format as the + # ordering attribute in models. + self.assertQuerysetEqual( + Article.objects.order_by("headline"), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) + self.assertQuerysetEqual( + Article.objects.order_by("pub_date", "-headline"), [ + "Article 1", + "Article 3", + "Article 2", + "Article 4", + ], + attrgetter("headline") + ) + + # Only the last order_by has any effect (since they each override any + # previous ordering). + self.assertQuerysetEqual( + Article.objects.order_by("id"), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) + self.assertQuerysetEqual( + Article.objects.order_by("id").order_by("-headline"), [ + "Article 4", + "Article 3", + "Article 2", + "Article 1", + ], + attrgetter("headline") + ) + + # Use the 'stop' part of slicing notation to limit the results. + self.assertQuerysetEqual( + Article.objects.order_by("headline")[:2], [ + "Article 1", + "Article 2", + ], + attrgetter("headline") + ) + + # Use the 'stop' and 'start' parts of slicing notation to offset the + # result list. + self.assertQuerysetEqual( + Article.objects.order_by("headline")[1:3], [ + "Article 2", + "Article 3", + ], + attrgetter("headline") + ) + + # Getting a single item should work too: + self.assertEqual(Article.objects.all()[0], a4) + + # Use '?' to order randomly. + self.assertEqual( + len(list(Article.objects.order_by("?"))), 4 + ) + + # Ordering can be reversed using the reverse() method on a queryset. + # This allows you to extract things like "the last two items" (reverse + # and then take the first two). + self.assertQuerysetEqual( + Article.objects.all().reverse()[:2], [ + "Article 1", + "Article 3", + ], + attrgetter("headline") + ) + + # Ordering can be based on fields included from an 'extra' clause + self.assertQuerysetEqual( + Article.objects.extra(select={"foo": "pub_date"}, order_by=["foo", "headline"]), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) + + # If the extra clause uses an SQL keyword for a name, it will be + # protected by quoting. + self.assertQuerysetEqual( + Article.objects.extra(select={"order": "pub_date"}, order_by=["order", "headline"]), [ + "Article 1", + "Article 2", + "Article 3", + "Article 4", + ], + attrgetter("headline") + ) diff --git a/tests/modeltests/pagination/models.py b/tests/modeltests/pagination/models.py index 9b79a6a74e19..48484dd59baa 100644 --- a/tests/modeltests/pagination/models.py +++ b/tests/modeltests/pagination/models.py @@ -8,173 +8,10 @@ from django.db import models + class Article(models.Model): headline = models.CharField(max_length=100, default='Default headline') pub_date = models.DateTimeField() def __unicode__(self): return self.headline - -__test__ = {'API_TESTS':""" -# Prepare a list of objects for pagination. ->>> from datetime import datetime ->>> for x in range(1, 10): -... a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) -... a.save() - -################## -# Paginator/Page # -################## - ->>> from django.core.paginator import Paginator ->>> paginator = Paginator(Article.objects.all(), 5) ->>> paginator.count -9 ->>> paginator.num_pages -2 ->>> paginator.page_range -[1, 2] - -# Get the first page. ->>> p = paginator.page(1) ->>> p - ->>> p.object_list -[, , , , ] ->>> p.has_next() -True ->>> p.has_previous() -False ->>> p.has_other_pages() -True ->>> p.next_page_number() -2 ->>> p.previous_page_number() -0 ->>> p.start_index() -1 ->>> p.end_index() -5 - -# Get the second page. ->>> p = paginator.page(2) ->>> p - ->>> p.object_list -[, , , ] ->>> p.has_next() -False ->>> p.has_previous() -True ->>> p.has_other_pages() -True ->>> p.next_page_number() -3 ->>> p.previous_page_number() -1 ->>> p.start_index() -6 ->>> p.end_index() -9 - -# Empty pages raise EmptyPage. ->>> paginator.page(0) -Traceback (most recent call last): -... -EmptyPage: ... ->>> paginator.page(3) -Traceback (most recent call last): -... -EmptyPage: ... - -# Empty paginators with allow_empty_first_page=True. ->>> paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=True) ->>> paginator.count -0 ->>> paginator.num_pages -1 ->>> paginator.page_range -[1] - -# Empty paginators with allow_empty_first_page=False. ->>> paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=False) ->>> paginator.count -0 ->>> paginator.num_pages -0 ->>> paginator.page_range -[] - -# Paginators work with regular lists/tuples, too -- not just with QuerySets. ->>> paginator = Paginator([1, 2, 3, 4, 5, 6, 7, 8, 9], 5) ->>> paginator.count -9 ->>> paginator.num_pages -2 ->>> paginator.page_range -[1, 2] - -# Get the first page. ->>> p = paginator.page(1) ->>> p - ->>> p.object_list -[1, 2, 3, 4, 5] ->>> p.has_next() -True ->>> p.has_previous() -False ->>> p.has_other_pages() -True ->>> p.next_page_number() -2 ->>> p.previous_page_number() -0 ->>> p.start_index() -1 ->>> p.end_index() -5 - -# Paginator can be passed other objects with a count() method. ->>> class CountContainer: -... def count(self): -... return 42 ->>> paginator = Paginator(CountContainer(), 10) ->>> paginator.count -42 ->>> paginator.num_pages -5 ->>> paginator.page_range -[1, 2, 3, 4, 5] - -# Paginator can be passed other objects that implement __len__. ->>> class LenContainer: -... def __len__(self): -... return 42 ->>> paginator = Paginator(LenContainer(), 10) ->>> paginator.count -42 ->>> paginator.num_pages -5 ->>> paginator.page_range -[1, 2, 3, 4, 5] - - -################## -# Orphan support # -################## - -# Add a few more records to test out the orphans feature. ->>> for x in range(10, 13): -... Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save() - -# With orphans set to 3 and 10 items per page, we should get all 12 items on a single page. ->>> paginator = Paginator(Article.objects.all(), 10, orphans=3) ->>> paginator.num_pages -1 - -# With orphans only set to 1, we should get two pages. ->>> paginator = Paginator(Article.objects.all(), 10, orphans=1) ->>> paginator.num_pages -2 -"""} diff --git a/tests/modeltests/pagination/tests.py b/tests/modeltests/pagination/tests.py new file mode 100644 index 000000000000..eaee46691326 --- /dev/null +++ b/tests/modeltests/pagination/tests.py @@ -0,0 +1,132 @@ +from datetime import datetime +from operator import attrgetter + +from django.core.paginator import Paginator, InvalidPage, EmptyPage +from django.test import TestCase + +from models import Article + + +class CountContainer(object): + def count(self): + return 42 + +class LenContainer(object): + def __len__(self): + return 42 + +class PaginationTests(TestCase): + def setUp(self): + # Prepare a list of objects for pagination. + for x in range(1, 10): + a = Article(headline='Article %s' % x, pub_date=datetime(2005, 7, 29)) + a.save() + + def test_paginator(self): + paginator = Paginator(Article.objects.all(), 5) + self.assertEqual(9, paginator.count) + self.assertEqual(2, paginator.num_pages) + self.assertEqual([1, 2], paginator.page_range) + + def test_first_page(self): + paginator = Paginator(Article.objects.all(), 5) + p = paginator.page(1) + self.assertEqual(u"", unicode(p)) + self.assertQuerysetEqual(p.object_list, [ + "", + "", + "", + "", + "" + ] + ) + self.assertTrue(p.has_next()) + self.assertFalse(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertEqual(2, p.next_page_number()) + self.assertEqual(0, p.previous_page_number()) + self.assertEqual(1, p.start_index()) + self.assertEqual(5, p.end_index()) + + def test_last_page(self): + paginator = Paginator(Article.objects.all(), 5) + p = paginator.page(2) + self.assertEqual(u"", unicode(p)) + self.assertQuerysetEqual(p.object_list, [ + "", + "", + "", + "" + ] + ) + self.assertFalse(p.has_next()) + self.assertTrue(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertEqual(3, p.next_page_number()) + self.assertEqual(1, p.previous_page_number()) + self.assertEqual(6, p.start_index()) + self.assertEqual(9, p.end_index()) + + def test_empty_page(self): + paginator = Paginator(Article.objects.all(), 5) + self.assertRaises(EmptyPage, paginator.page, 0) + self.assertRaises(EmptyPage, paginator.page, 3) + + # Empty paginators with allow_empty_first_page=True. + paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=True) + self.assertEqual(0, paginator.count) + self.assertEqual(1, paginator.num_pages) + self.assertEqual([1], paginator.page_range) + + # Empty paginators with allow_empty_first_page=False. + paginator = Paginator(Article.objects.filter(id=0), 5, allow_empty_first_page=False) + self.assertEqual(0, paginator.count) + self.assertEqual(0, paginator.num_pages) + self.assertEqual([], paginator.page_range) + + def test_invalid_page(self): + paginator = Paginator(Article.objects.all(), 5) + self.assertRaises(InvalidPage, paginator.page, 7) + + def test_orphans(self): + # Add a few more records to test out the orphans feature. + for x in range(10, 13): + Article(headline="Article %s" % x, pub_date=datetime(2006, 10, 6)).save() + + # With orphans set to 3 and 10 items per page, we should get all 12 items on a single page. + paginator = Paginator(Article.objects.all(), 10, orphans=3) + self.assertEqual(1, paginator.num_pages) + + # With orphans only set to 1, we should get two pages. + paginator = Paginator(Article.objects.all(), 10, orphans=1) + self.assertEqual(2, paginator.num_pages) + + def test_paginate_list(self): + # Paginators work with regular lists/tuples, too -- not just with QuerySets. + paginator = Paginator([1, 2, 3, 4, 5, 6, 7, 8, 9], 5) + self.assertEqual(9, paginator.count) + self.assertEqual(2, paginator.num_pages) + self.assertEqual([1, 2], paginator.page_range) + p = paginator.page(1) + self.assertEqual(u"", unicode(p)) + self.assertEqual([1, 2, 3, 4, 5], p.object_list) + self.assertTrue(p.has_next()) + self.assertFalse(p.has_previous()) + self.assertTrue(p.has_other_pages()) + self.assertEqual(2, p.next_page_number()) + self.assertEqual(0, p.previous_page_number()) + self.assertEqual(1, p.start_index()) + self.assertEqual(5, p.end_index()) + + def test_paginate_misc_classes(self): + # Paginator can be passed other objects with a count() method. + paginator = Paginator(CountContainer(), 10) + self.assertEqual(42, paginator.count) + self.assertEqual(5, paginator.num_pages) + self.assertEqual([1, 2, 3, 4, 5], paginator.page_range) + + # Paginator can be passed other objects that implement __len__. + paginator = Paginator(LenContainer(), 10) + self.assertEqual(42, paginator.count) + self.assertEqual(5, paginator.num_pages) + self.assertEqual([1, 2, 3, 4, 5], paginator.page_range) diff --git a/tests/modeltests/properties/models.py b/tests/modeltests/properties/models.py index 5326e4ec5fce..390efe3a22ea 100644 --- a/tests/modeltests/properties/models.py +++ b/tests/modeltests/properties/models.py @@ -19,22 +19,3 @@ def _set_full_name(self, combined_name): full_name = property(_get_full_name) full_name_2 = property(_get_full_name, _set_full_name) - -__test__ = {'API_TESTS':""" ->>> a = Person(first_name='John', last_name='Lennon') ->>> a.save() ->>> a.full_name -'John Lennon' - -# The "full_name" property hasn't provided a "set" method. ->>> a.full_name = 'Paul McCartney' -Traceback (most recent call last): - ... -AttributeError: can't set attribute - -# But "full_name_2" has, and it can be used to initialise the class. ->>> a2 = Person(full_name_2 = 'Paul McCartney') ->>> a2.save() ->>> a2.first_name -'Paul' -"""} diff --git a/tests/modeltests/properties/tests.py b/tests/modeltests/properties/tests.py new file mode 100644 index 000000000000..e31ac58d4d46 --- /dev/null +++ b/tests/modeltests/properties/tests.py @@ -0,0 +1,20 @@ +from django.test import TestCase +from models import Person + +class PropertyTests(TestCase): + + def setUp(self): + self.a = Person(first_name='John', last_name='Lennon') + self.a.save() + + def test_getter(self): + self.assertEqual(self.a.full_name, 'John Lennon') + + def test_setter(self): + # The "full_name" property hasn't provided a "set" method. + self.assertRaises(AttributeError, setattr, self.a, 'full_name', 'Paul McCartney') + + # But "full_name_2" has, and it can be used to initialise the class. + a2 = Person(full_name_2 = 'Paul McCartney') + a2.save() + self.assertEqual(a2.first_name, 'Paul') diff --git a/tests/modeltests/proxy_model_inheritance/tests.py b/tests/modeltests/proxy_model_inheritance/tests.py index d10d6a4ac15d..b6828515ab9a 100644 --- a/tests/modeltests/proxy_model_inheritance/tests.py +++ b/tests/modeltests/proxy_model_inheritance/tests.py @@ -17,7 +17,7 @@ class ProxyModelInheritanceTests(TransactionTestCase): def setUp(self): - self.old_sys_path = sys.path + self.old_sys_path = sys.path[:] sys.path.append(os.path.dirname(os.path.abspath(__file__))) self.old_installed_apps = settings.INSTALLED_APPS settings.INSTALLED_APPS = ('app1', 'app2') diff --git a/tests/modeltests/proxy_models/models.py b/tests/modeltests/proxy_models/models.py index 28446b96c1d3..90d54d94dd72 100644 --- a/tests/modeltests/proxy_models/models.py +++ b/tests/modeltests/proxy_models/models.py @@ -161,232 +161,4 @@ class Improvement(Issue): class ProxyImprovement(Improvement): class Meta: - proxy = True - -__test__ = {'API_TESTS' : """ -# The MyPerson model should be generating the same database queries as the -# Person model (when the same manager is used in each case). ->>> from django.db import DEFAULT_DB_ALIAS ->>> MyPerson.other.all().query.get_compiler(DEFAULT_DB_ALIAS).as_sql() == Person.objects.order_by("name").query.get_compiler(DEFAULT_DB_ALIAS).as_sql() -True - -# The StatusPerson models should have its own table (it's using ORM-level -# inheritance). ->>> StatusPerson.objects.all().query.get_compiler(DEFAULT_DB_ALIAS).as_sql() == Person.objects.all().query.get_compiler(DEFAULT_DB_ALIAS).as_sql() -False - -# Creating a Person makes them accessible through the MyPerson proxy. ->>> _ = Person.objects.create(name="Foo McBar") ->>> len(Person.objects.all()) -1 ->>> len(MyPerson.objects.all()) -1 ->>> MyPerson.objects.get(name="Foo McBar").id -1 ->>> MyPerson.objects.get(id=1).has_special_name() -False - -# Person is not proxied by StatusPerson subclass, however. ->>> StatusPerson.objects.all() -[] - -# A new MyPerson also shows up as a standard Person ->>> _ = MyPerson.objects.create(name="Bazza del Frob") ->>> len(MyPerson.objects.all()) -2 ->>> len(Person.objects.all()) -2 - ->>> _ = LowerStatusPerson.objects.create(status="low", name="homer") ->>> LowerStatusPerson.objects.all() -[] - -# Correct type when querying a proxy of proxy - ->>> MyPersonProxy.objects.all() -[, , ] - -# Proxy models are included in the ancestors for a model's DoesNotExist and MultipleObjectsReturned ->>> try: -... MyPersonProxy.objects.get(name='Zathras') -... except Person.DoesNotExist: -... pass ->>> try: -... MyPersonProxy.objects.get(id__lt=10) -... except Person.MultipleObjectsReturned: -... pass ->>> try: -... StatusPerson.objects.get(name='Zathras') -... except Person.DoesNotExist: -... pass ->>> sp1 = StatusPerson.objects.create(name='Bazza Jr.') ->>> sp2 = StatusPerson.objects.create(name='Foo Jr.') ->>> try: -... StatusPerson.objects.get(id__lt=10) -... except Person.MultipleObjectsReturned: -... pass - -# And now for some things that shouldn't work... -# -# All base classes must be non-abstract ->>> class NoAbstract(Abstract): -... class Meta: -... proxy = True -Traceback (most recent call last): - .... -TypeError: Abstract base class containing model fields not permitted for proxy model 'NoAbstract'. - -# The proxy must actually have one concrete base class ->>> class TooManyBases(Person, Abstract): -... class Meta: -... proxy = True -Traceback (most recent call last): - .... -TypeError: Abstract base class containing model fields not permitted for proxy model 'TooManyBases'. - ->>> class NoBaseClasses(models.Model): -... class Meta: -... proxy = True -Traceback (most recent call last): - .... -TypeError: Proxy model 'NoBaseClasses' has no non-abstract model base class. - - -# A proxy cannot introduce any new fields ->>> class NoNewFields(Person): -... newfield = models.BooleanField() -... class Meta: -... proxy = True -Traceback (most recent call last): - .... -FieldError: Proxy model 'NoNewFields' contains model fields. - -# Manager tests. - ->>> Person.objects.all().delete() ->>> _ = Person.objects.create(name="fred") ->>> _ = Person.objects.create(name="wilma") ->>> _ = Person.objects.create(name="barney") - ->>> MyPerson.objects.all() -[, ] ->>> MyPerson._default_manager.all() -[, ] - ->>> OtherPerson.objects.all() -[, ] ->>> OtherPerson.excluder.all() -[, ] ->>> OtherPerson._default_manager.all() -[, ] - -# Test save signals for proxy models ->>> from django.db.models import signals ->>> def make_handler(model, event): -... def _handler(*args, **kwargs): -... print u"%s %s save" % (model, event) -... return _handler ->>> h1 = make_handler('MyPerson', 'pre') ->>> h2 = make_handler('MyPerson', 'post') ->>> h3 = make_handler('Person', 'pre') ->>> h4 = make_handler('Person', 'post') ->>> signals.pre_save.connect(h1, sender=MyPerson) ->>> signals.post_save.connect(h2, sender=MyPerson) ->>> signals.pre_save.connect(h3, sender=Person) ->>> signals.post_save.connect(h4, sender=Person) ->>> dino = MyPerson.objects.create(name=u"dino") -MyPerson pre save -MyPerson post save - -# Test save signals for proxy proxy models ->>> h5 = make_handler('MyPersonProxy', 'pre') ->>> h6 = make_handler('MyPersonProxy', 'post') ->>> signals.pre_save.connect(h5, sender=MyPersonProxy) ->>> signals.post_save.connect(h6, sender=MyPersonProxy) ->>> dino = MyPersonProxy.objects.create(name=u"pebbles") -MyPersonProxy pre save -MyPersonProxy post save - ->>> signals.pre_save.disconnect(h1, sender=MyPerson) ->>> signals.post_save.disconnect(h2, sender=MyPerson) ->>> signals.pre_save.disconnect(h3, sender=Person) ->>> signals.post_save.disconnect(h4, sender=Person) ->>> signals.pre_save.disconnect(h5, sender=MyPersonProxy) ->>> signals.post_save.disconnect(h6, sender=MyPersonProxy) - -# A proxy has the same content type as the model it is proxying for (at the -# storage level, it is meant to be essentially indistinguishable). ->>> ctype = ContentType.objects.get_for_model ->>> ctype(Person) is ctype(OtherPerson) -True - ->>> MyPersonProxy.objects.all() -[, , , ] - ->>> u = User.objects.create(name='Bruce') ->>> User.objects.all() -[] ->>> UserProxy.objects.all() -[] ->>> UserProxyProxy.objects.all() -[] - -# Proxy objects can be deleted ->>> u2 = UserProxy.objects.create(name='George') ->>> UserProxy.objects.all() -[, ] ->>> u2.delete() ->>> UserProxy.objects.all() -[] - - -# We can still use `select_related()` to include related models in our querysets. ->>> country = Country.objects.create(name='Australia') ->>> state = State.objects.create(name='New South Wales', country=country) - ->>> State.objects.select_related() -[] ->>> StateProxy.objects.select_related() -[] ->>> StateProxy.objects.get(name='New South Wales') - ->>> StateProxy.objects.select_related().get(name='New South Wales') - - ->>> contributor = TrackerUser.objects.create(name='Contributor',status='contrib') ->>> someone = BaseUser.objects.create(name='Someone') ->>> _ = Bug.objects.create(summary='fix this', version='1.1beta', -... assignee=contributor, reporter=someone) ->>> pcontributor = ProxyTrackerUser.objects.create(name='OtherContributor', -... status='proxy') ->>> _ = Improvement.objects.create(summary='improve that', version='1.1beta', -... assignee=contributor, reporter=pcontributor, -... associated_bug=ProxyProxyBug.objects.all()[0]) - -# Related field filter on proxy ->>> ProxyBug.objects.get(version__icontains='beta') - - -# Select related + filter on proxy ->>> ProxyBug.objects.select_related().get(version__icontains='beta') - - -# Proxy of proxy, select_related + filter ->>> ProxyProxyBug.objects.select_related().get(version__icontains='beta') - - -# Select related + filter on a related proxy field ->>> ProxyImprovement.objects.select_related().get(reporter__name__icontains='butor') - - -# Select related + filter on a related proxy of proxy field ->>> ProxyImprovement.objects.select_related().get(associated_bug__summary__icontains='fix') - - -Proxy models can be loaded from fixtures (Regression for #11194) ->>> from django.core import management ->>> management.call_command('loaddata', 'mypeople.json', verbosity=0) ->>> MyPerson.objects.get(pk=100) - - -"""} + proxy = True \ No newline at end of file diff --git a/tests/modeltests/proxy_models/tests.py b/tests/modeltests/proxy_models/tests.py new file mode 100644 index 000000000000..0a46a252c5ff --- /dev/null +++ b/tests/modeltests/proxy_models/tests.py @@ -0,0 +1,314 @@ +from django.test import TestCase +from django.db import models, DEFAULT_DB_ALIAS +from django.db.models import signals +from django.core import management +from django.core.exceptions import FieldError + +from django.contrib.contenttypes.models import ContentType + +from models import MyPerson, Person, StatusPerson, LowerStatusPerson +from models import MyPersonProxy, Abstract, OtherPerson, User, UserProxy +from models import UserProxyProxy, Country, State, StateProxy, TrackerUser +from models import BaseUser, Bug, ProxyTrackerUser, Improvement, ProxyProxyBug +from models import ProxyBug, ProxyImprovement + +class ProxyModelTests(TestCase): + def test_same_manager_queries(self): + """ + The MyPerson model should be generating the same database queries as + the Person model (when the same manager is used in each case). + """ + my_person_sql = MyPerson.other.all().query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + person_sql = Person.objects.order_by("name").query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + self.assertEqual(my_person_sql, person_sql) + + def test_inheretance_new_table(self): + """ + The StatusPerson models should have its own table (it's using ORM-level + inheritance). + """ + sp_sql = StatusPerson.objects.all().query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + p_sql = Person.objects.all().query.get_compiler( + DEFAULT_DB_ALIAS).as_sql() + self.assertNotEqual(sp_sql, p_sql) + + def test_basic_proxy(self): + """ + Creating a Person makes them accessible through the MyPerson proxy. + """ + person = Person.objects.create(name="Foo McBar") + self.assertEqual(len(Person.objects.all()), 1) + self.assertEqual(len(MyPerson.objects.all()), 1) + self.assertEqual(MyPerson.objects.get(name="Foo McBar").id, person.id) + self.assertFalse(MyPerson.objects.get(id=person.id).has_special_name()) + + def test_no_proxy(self): + """ + Person is not proxied by StatusPerson subclass. + """ + Person.objects.create(name="Foo McBar") + self.assertEqual(list(StatusPerson.objects.all()), []) + + def test_basic_proxy_reverse(self): + """ + A new MyPerson also shows up as a standard Person. + """ + MyPerson.objects.create(name="Bazza del Frob") + self.assertEqual(len(MyPerson.objects.all()), 1) + self.assertEqual(len(Person.objects.all()), 1) + + LowerStatusPerson.objects.create(status="low", name="homer") + lsps = [lsp.name for lsp in LowerStatusPerson.objects.all()] + self.assertEqual(lsps, ["homer"]) + + def test_correct_type_proxy_of_proxy(self): + """ + Correct type when querying a proxy of proxy + """ + Person.objects.create(name="Foo McBar") + MyPerson.objects.create(name="Bazza del Frob") + LowerStatusPerson.objects.create(status="low", name="homer") + pp = sorted([mpp.name for mpp in MyPersonProxy.objects.all()]) + self.assertEqual(pp, ['Bazza del Frob', 'Foo McBar', 'homer']) + + def test_proxy_included_in_ancestors(self): + """ + Proxy models are included in the ancestors for a model's DoesNotExist + and MultipleObjectsReturned + """ + Person.objects.create(name="Foo McBar") + MyPerson.objects.create(name="Bazza del Frob") + LowerStatusPerson.objects.create(status="low", name="homer") + max_id = Person.objects.aggregate(max_id=models.Max('id'))['max_id'] + + self.assertRaises(Person.DoesNotExist, + MyPersonProxy.objects.get, + name='Zathras' + ) + self.assertRaises(Person.MultipleObjectsReturned, + MyPersonProxy.objects.get, + id__lt=max_id+1 + ) + self.assertRaises(Person.DoesNotExist, + StatusPerson.objects.get, + name='Zathras' + ) + + sp1 = StatusPerson.objects.create(name='Bazza Jr.') + sp2 = StatusPerson.objects.create(name='Foo Jr.') + max_id = Person.objects.aggregate(max_id=models.Max('id'))['max_id'] + + self.assertRaises(Person.MultipleObjectsReturned, + StatusPerson.objects.get, + id__lt=max_id+1 + ) + + def test_abc(self): + """ + All base classes must be non-abstract + """ + def build_abc(): + class NoAbstract(Abstract): + class Meta: + proxy = True + self.assertRaises(TypeError, build_abc) + + def test_no_cbc(self): + """ + The proxy must actually have one concrete base class + """ + def build_no_cbc(): + class TooManyBases(Person, Abstract): + class Meta: + proxy = True + self.assertRaises(TypeError, build_no_cbc) + + def test_no_base_classes(self): + def build_no_base_classes(): + class NoBaseClasses(models.Model): + class Meta: + proxy = True + self.assertRaises(TypeError, build_no_base_classes) + + def test_new_fields(self): + def build_new_fields(): + class NoNewFields(Person): + newfield = models.BooleanField() + class Meta: + proxy = True + self.assertRaises(FieldError, build_new_fields) + + def test_myperson_manager(self): + Person.objects.create(name="fred") + Person.objects.create(name="wilma") + Person.objects.create(name="barney") + + resp = [p.name for p in MyPerson.objects.all()] + self.assertEqual(resp, ['barney', 'fred']) + + resp = [p.name for p in MyPerson._default_manager.all()] + self.assertEqual(resp, ['barney', 'fred']) + + def test_otherperson_manager(self): + Person.objects.create(name="fred") + Person.objects.create(name="wilma") + Person.objects.create(name="barney") + + resp = [p.name for p in OtherPerson.objects.all()] + self.assertEqual(resp, ['barney', 'wilma']) + + resp = [p.name for p in OtherPerson.excluder.all()] + self.assertEqual(resp, ['barney', 'fred']) + + resp = [p.name for p in OtherPerson._default_manager.all()] + self.assertEqual(resp, ['barney', 'wilma']) + + def test_proxy_model_signals(self): + """ + Test save signals for proxy models + """ + output = [] + + def make_handler(model, event): + def _handler(*args, **kwargs): + output.append('%s %s save' % (model, event)) + return _handler + + h1 = make_handler('MyPerson', 'pre') + h2 = make_handler('MyPerson', 'post') + h3 = make_handler('Person', 'pre') + h4 = make_handler('Person', 'post') + + signals.pre_save.connect(h1, sender=MyPerson) + signals.post_save.connect(h2, sender=MyPerson) + signals.pre_save.connect(h3, sender=Person) + signals.post_save.connect(h4, sender=Person) + + dino = MyPerson.objects.create(name=u"dino") + self.assertEqual(output, [ + 'MyPerson pre save', + 'MyPerson post save' + ]) + + output = [] + + h5 = make_handler('MyPersonProxy', 'pre') + h6 = make_handler('MyPersonProxy', 'post') + + signals.pre_save.connect(h5, sender=MyPersonProxy) + signals.post_save.connect(h6, sender=MyPersonProxy) + + dino = MyPersonProxy.objects.create(name=u"pebbles") + + self.assertEqual(output, [ + 'MyPersonProxy pre save', + 'MyPersonProxy post save' + ]) + + signals.pre_save.disconnect(h1, sender=MyPerson) + signals.post_save.disconnect(h2, sender=MyPerson) + signals.pre_save.disconnect(h3, sender=Person) + signals.post_save.disconnect(h4, sender=Person) + signals.pre_save.disconnect(h5, sender=MyPersonProxy) + signals.post_save.disconnect(h6, sender=MyPersonProxy) + + def test_content_type(self): + ctype = ContentType.objects.get_for_model + self.assertTrue(ctype(Person) is ctype(OtherPerson)) + + def test_user_userproxy_userproxyproxy(self): + User.objects.create(name='Bruce') + + resp = [u.name for u in User.objects.all()] + self.assertEqual(resp, ['Bruce']) + + resp = [u.name for u in UserProxy.objects.all()] + self.assertEqual(resp, ['Bruce']) + + resp = [u.name for u in UserProxyProxy.objects.all()] + self.assertEqual(resp, ['Bruce']) + + def test_proxy_delete(self): + """ + Proxy objects can be deleted + """ + User.objects.create(name='Bruce') + u2 = UserProxy.objects.create(name='George') + + resp = [u.name for u in UserProxy.objects.all()] + self.assertEqual(resp, ['Bruce', 'George']) + + u2.delete() + + resp = [u.name for u in UserProxy.objects.all()] + self.assertEqual(resp, ['Bruce']) + + def test_select_related(self): + """ + We can still use `select_related()` to include related models in our + querysets. + """ + country = Country.objects.create(name='Australia') + state = State.objects.create(name='New South Wales', country=country) + + resp = [s.name for s in State.objects.select_related()] + self.assertEqual(resp, ['New South Wales']) + + resp = [s.name for s in StateProxy.objects.select_related()] + self.assertEqual(resp, ['New South Wales']) + + self.assertEqual(StateProxy.objects.get(name='New South Wales').name, + 'New South Wales') + + resp = StateProxy.objects.select_related().get(name='New South Wales') + self.assertEqual(resp.name, 'New South Wales') + + def test_proxy_bug(self): + contributor = TrackerUser.objects.create(name='Contributor', + status='contrib') + someone = BaseUser.objects.create(name='Someone') + Bug.objects.create(summary='fix this', version='1.1beta', + assignee=contributor, reporter=someone) + pcontributor = ProxyTrackerUser.objects.create(name='OtherContributor', + status='proxy') + Improvement.objects.create(summary='improve that', version='1.1beta', + assignee=contributor, reporter=pcontributor, + associated_bug=ProxyProxyBug.objects.all()[0]) + + # Related field filter on proxy + resp = ProxyBug.objects.get(version__icontains='beta') + self.assertEqual(repr(resp), '') + + # Select related + filter on proxy + resp = ProxyBug.objects.select_related().get(version__icontains='beta') + self.assertEqual(repr(resp), '') + + # Proxy of proxy, select_related + filter + resp = ProxyProxyBug.objects.select_related().get( + version__icontains='beta' + ) + self.assertEqual(repr(resp), '') + + # Select related + filter on a related proxy field + resp = ProxyImprovement.objects.select_related().get( + reporter__name__icontains='butor' + ) + self.assertEqual(repr(resp), + '' + ) + + # Select related + filter on a related proxy of proxy field + resp = ProxyImprovement.objects.select_related().get( + associated_bug__summary__icontains='fix' + ) + self.assertEqual(repr(resp), + '' + ) + + def test_proxy_load_from_fixture(self): + management.call_command('loaddata', 'mypeople.json', verbosity=0, commit=False) + p = MyPerson.objects.get(pk=100) + self.assertEqual(p.name, 'Elvis Presley') diff --git a/tests/modeltests/raw_query/tests.py b/tests/modeltests/raw_query/tests.py index a0325eff97f2..a1e7edb7858d 100644 --- a/tests/modeltests/raw_query/tests.py +++ b/tests/modeltests/raw_query/tests.py @@ -1,5 +1,7 @@ from datetime import date +from django.conf import settings +from django.db import connection from django.db.models.sql.query import InvalidQuery from django.test import TestCase @@ -53,6 +55,16 @@ def assertAnnotations(self, results, expected_annotations): self.assertTrue(hasattr(result, annotation)) self.assertEqual(getattr(result, annotation), value) + def assert_num_queries(self, n, func, *args, **kwargs): + old_DEBUG = settings.DEBUG + settings.DEBUG = True + starting_queries = len(connection.queries) + try: + func(*args, **kwargs) + finally: + settings.DEBUG = old_DEBUG + self.assertEqual(starting_queries + n, len(connection.queries)) + def testSimpleRawQuery(self): """ Basic test of raw query with a simple database query @@ -217,3 +229,8 @@ def test_inheritance(self): self.assertEqual( [o.pk for o in FriendlyAuthor.objects.raw(query)], [f.pk] ) + + def test_query_count(self): + self.assert_num_queries(1, + list, Author.objects.raw("SELECT * FROM raw_query_author") + ) diff --git a/tests/modeltests/reserved_names/models.py b/tests/modeltests/reserved_names/models.py index f698b5bc4940..d8c1238ff10d 100644 --- a/tests/modeltests/reserved_names/models.py +++ b/tests/modeltests/reserved_names/models.py @@ -22,33 +22,4 @@ class Meta: db_table = 'select' def __unicode__(self): - return self.when - -__test__ = {'API_TESTS':""" ->>> import datetime ->>> day1 = datetime.date(2005, 1, 1) ->>> day2 = datetime.date(2006, 2, 2) ->>> t = Thing(when='a', join='b', like='c', drop='d', alter='e', having='f', where=day1, has_hyphen='h') ->>> t.save() ->>> print t.when -a - ->>> u = Thing(when='h', join='i', like='j', drop='k', alter='l', having='m', where=day2) ->>> u.save() ->>> print u.when -h - ->>> Thing.objects.order_by('when') -[, ] ->>> v = Thing.objects.get(pk='a') ->>> print v.join -b ->>> print v.where -2005-01-01 - ->>> Thing.objects.dates('where', 'year') -[datetime.datetime(2005, 1, 1, 0, 0), datetime.datetime(2006, 1, 1, 0, 0)] - ->>> Thing.objects.filter(where__month=1) -[] -"""} + return self.when \ No newline at end of file diff --git a/tests/modeltests/reserved_names/tests.py b/tests/modeltests/reserved_names/tests.py new file mode 100644 index 000000000000..b7e48674119b --- /dev/null +++ b/tests/modeltests/reserved_names/tests.py @@ -0,0 +1,48 @@ +import datetime + +from django.test import TestCase + +from models import Thing + +class ReservedNameTests(TestCase): + def generate(self): + day1 = datetime.date(2005, 1, 1) + t = Thing.objects.create(when='a', join='b', like='c', drop='d', + alter='e', having='f', where=day1, has_hyphen='h') + day2 = datetime.date(2006, 2, 2) + u = Thing.objects.create(when='h', join='i', like='j', drop='k', + alter='l', having='m', where=day2) + + def test_simple(self): + day1 = datetime.date(2005, 1, 1) + t = Thing.objects.create(when='a', join='b', like='c', drop='d', + alter='e', having='f', where=day1, has_hyphen='h') + self.assertEqual(t.when, 'a') + + day2 = datetime.date(2006, 2, 2) + u = Thing.objects.create(when='h', join='i', like='j', drop='k', + alter='l', having='m', where=day2) + self.assertEqual(u.when, 'h') + + def test_order_by(self): + self.generate() + things = [t.when for t in Thing.objects.order_by('when')] + self.assertEqual(things, ['a', 'h']) + + def test_fields(self): + self.generate() + v = Thing.objects.get(pk='a') + self.assertEqual(v.join, 'b') + self.assertEqual(v.where, datetime.date(year=2005, month=1, day=1)) + + def test_dates(self): + self.generate() + resp = Thing.objects.dates('where', 'year') + self.assertEqual(list(resp), [ + datetime.datetime(2005, 1, 1, 0, 0), + datetime.datetime(2006, 1, 1, 0, 0), + ]) + + def test_month_filter(self): + self.generate() + self.assertEqual(Thing.objects.filter(where__month=1)[0].when, 'a') diff --git a/tests/modeltests/reverse_lookup/models.py b/tests/modeltests/reverse_lookup/models.py index ef385b4b1886..2ffdc39b3be1 100644 --- a/tests/modeltests/reverse_lookup/models.py +++ b/tests/modeltests/reverse_lookup/models.py @@ -26,34 +26,3 @@ class Choice(models.Model): def __unicode__(self): return self.name - -__test__ = {'API_TESTS':""" ->>> john = User(name="John Doe") ->>> john.save() ->>> jim = User(name="Jim Bo") ->>> jim.save() ->>> first_poll = Poll(question="What's the first question?", creator=john) ->>> first_poll.save() ->>> second_poll = Poll(question="What's the second question?", creator=jim) ->>> second_poll.save() ->>> new_choice = Choice(poll=first_poll, related_poll=second_poll, name="This is the answer.") ->>> new_choice.save() - ->>> # Reverse lookups by field name: ->>> User.objects.get(poll__question__exact="What's the first question?") - ->>> User.objects.get(poll__question__exact="What's the second question?") - - ->>> # Reverse lookups by related_name: ->>> Poll.objects.get(poll_choice__name__exact="This is the answer.") - ->>> Poll.objects.get(related_choice__name__exact="This is the answer.") - - ->>> # If a related_name is given you can't use the field name instead: ->>> Poll.objects.get(choice__name__exact="This is the answer") -Traceback (most recent call last): - ... -FieldError: Cannot resolve keyword 'choice' into field. Choices are: creator, id, poll_choice, question, related_choice -"""} diff --git a/tests/modeltests/reverse_lookup/tests.py b/tests/modeltests/reverse_lookup/tests.py new file mode 100644 index 000000000000..9a6e3068fab0 --- /dev/null +++ b/tests/modeltests/reverse_lookup/tests.py @@ -0,0 +1,49 @@ +from django.test import TestCase +from django.core.exceptions import FieldError + +from models import User, Poll, Choice + +class ReverseLookupTests(TestCase): + + def setUp(self): + john = User.objects.create(name="John Doe") + jim = User.objects.create(name="Jim Bo") + first_poll = Poll.objects.create( + question="What's the first question?", + creator=john + ) + second_poll = Poll.objects.create( + question="What's the second question?", + creator=jim + ) + new_choice = Choice.objects.create( + poll=first_poll, + related_poll=second_poll, + name="This is the answer." + ) + + def test_reverse_by_field(self): + u1 = User.objects.get( + poll__question__exact="What's the first question?" + ) + self.assertEqual(u1.name, "John Doe") + + u2 = User.objects.get( + poll__question__exact="What's the second question?" + ) + self.assertEqual(u2.name, "Jim Bo") + + def test_reverse_by_related_name(self): + p1 = Poll.objects.get(poll_choice__name__exact="This is the answer.") + self.assertEqual(p1.question, "What's the first question?") + + p2 = Poll.objects.get( + related_choice__name__exact="This is the answer.") + self.assertEqual(p2.question, "What's the second question?") + + def test_reverse_field_name_disallowed(self): + """ + If a related_name is given you can't use the field name instead + """ + self.assertRaises(FieldError, Poll.objects.get, + choice__name__exact="This is the answer") diff --git a/tests/modeltests/save_delete_hooks/models.py b/tests/modeltests/save_delete_hooks/models.py index 54c9defd8b40..515c7f6c917e 100644 --- a/tests/modeltests/save_delete_hooks/models.py +++ b/tests/modeltests/save_delete_hooks/models.py @@ -7,37 +7,26 @@ from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=20) last_name = models.CharField(max_length=20) + def __init__(self, *args, **kwargs): + super(Person, self).__init__(*args, **kwargs) + self.data = [] + def __unicode__(self): return u"%s %s" % (self.first_name, self.last_name) - def save(self, force_insert=False, force_update=False): - print "Before save" + def save(self, *args, **kwargs): + self.data.append("Before save") # Call the "real" save() method - super(Person, self).save(force_insert, force_update) - print "After save" + super(Person, self).save(*args, **kwargs) + self.data.append("After save") def delete(self): - print "Before deletion" - super(Person, self).delete() # Call the "real" delete() method - print "After deletion" - -__test__ = {'API_TESTS':""" ->>> p1 = Person(first_name='John', last_name='Smith') ->>> p1.save() -Before save -After save - ->>> Person.objects.all() -[] - ->>> p1.delete() -Before deletion -After deletion - ->>> Person.objects.all() -[] -"""} + self.data.append("Before deletion") + # Call the "real" delete() method + super(Person, self).delete() + self.data.append("After deletion") diff --git a/tests/modeltests/save_delete_hooks/tests.py b/tests/modeltests/save_delete_hooks/tests.py new file mode 100644 index 000000000000..dc7b8ee12ab8 --- /dev/null +++ b/tests/modeltests/save_delete_hooks/tests.py @@ -0,0 +1,30 @@ +from django.test import TestCase + +from models import Person + + +class SaveDeleteHookTests(TestCase): + def test_basic(self): + p = Person(first_name="John", last_name="Smith") + self.assertEqual(p.data, []) + p.save() + self.assertEqual(p.data, [ + "Before save", + "After save", + ]) + + self.assertQuerysetEqual( + Person.objects.all(), [ + "John Smith", + ], + unicode + ) + + p.delete() + self.assertEqual(p.data, [ + "Before save", + "After save", + "Before deletion", + "After deletion", + ]) + self.assertQuerysetEqual(Person.objects.all(), []) diff --git a/tests/modeltests/select_related/models.py b/tests/modeltests/select_related/models.py index 9d64cf24c6c7..3c2e7721fd63 100644 --- a/tests/modeltests/select_related/models.py +++ b/tests/modeltests/select_related/models.py @@ -56,134 +56,4 @@ class Species(models.Model): name = models.CharField(max_length=50) genus = models.ForeignKey(Genus) def __unicode__(self): - return self.name - -def create_tree(stringtree): - """Helper to create a complete tree""" - names = stringtree.split() - models = [Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species] - assert len(names) == len(models), (names, models) - - parent = None - for name, model in zip(names, models): - try: - obj = model.objects.get(name=name) - except model.DoesNotExist: - obj = model(name=name) - if parent: - setattr(obj, parent.__class__.__name__.lower(), parent) - obj.save() - parent = obj - -__test__ = {'API_TESTS':""" - -# Set up. -# The test runner sets settings.DEBUG to False, but we want to gather queries -# so we'll set it to True here and reset it at the end of the test suite. ->>> from django.conf import settings ->>> settings.DEBUG = True - ->>> create_tree("Eukaryota Animalia Anthropoda Insecta Diptera Drosophilidae Drosophila melanogaster") ->>> create_tree("Eukaryota Animalia Chordata Mammalia Primates Hominidae Homo sapiens") ->>> create_tree("Eukaryota Plantae Magnoliophyta Magnoliopsida Fabales Fabaceae Pisum sativum") ->>> create_tree("Eukaryota Fungi Basidiomycota Homobasidiomycatae Agaricales Amanitacae Amanita muscaria") - ->>> from django import db - -# Normally, accessing FKs doesn't fill in related objects: ->>> db.reset_queries() ->>> fly = Species.objects.get(name="melanogaster") ->>> fly.genus.family.order.klass.phylum.kingdom.domain - ->>> len(db.connection.queries) -8 - -# However, a select_related() call will fill in those related objects without any extra queries: ->>> db.reset_queries() ->>> person = Species.objects.select_related(depth=10).get(name="sapiens") ->>> person.genus.family.order.klass.phylum.kingdom.domain - ->>> len(db.connection.queries) -1 - -# select_related() also of course applies to entire lists, not just items. -# Without select_related() ->>> db.reset_queries() ->>> world = Species.objects.all() ->>> [o.genus.family for o in world] -[, , , ] ->>> len(db.connection.queries) -9 - -# With select_related(): ->>> db.reset_queries() ->>> world = Species.objects.all().select_related() ->>> [o.genus.family for o in world] -[, , , ] ->>> len(db.connection.queries) -1 - -# The "depth" argument to select_related() will stop the descent at a particular level: ->>> db.reset_queries() ->>> pea = Species.objects.select_related(depth=1).get(name="sativum") ->>> pea.genus.family.order.klass.phylum.kingdom.domain - - -# Notice: one fewer queries than above because of depth=1 ->>> len(db.connection.queries) -7 - ->>> db.reset_queries() ->>> pea = Species.objects.select_related(depth=5).get(name="sativum") ->>> pea.genus.family.order.klass.phylum.kingdom.domain - ->>> len(db.connection.queries) -3 - ->>> db.reset_queries() ->>> world = Species.objects.all().select_related(depth=2) ->>> [o.genus.family.order for o in world] -[, , , ] ->>> len(db.connection.queries) -5 - ->>> s = Species.objects.all().select_related(depth=1).extra(select={'a': 'select_related_species.id + 10'})[0] ->>> s.id + 10 == s.a -True - -# The optional fields passed to select_related() control which related models -# we pull in. This allows for smaller queries and can act as an alternative -# (or, in addition to) the depth parameter. - -# In the next two cases, we explicitly say to select the 'genus' and -# 'genus.family' models, leading to the same number of queries as before. ->>> db.reset_queries() ->>> world = Species.objects.select_related('genus__family') ->>> [o.genus.family for o in world] -[, , , ] ->>> len(db.connection.queries) -1 - ->>> db.reset_queries() ->>> world = Species.objects.filter(genus__name='Amanita').select_related('genus__family') ->>> [o.genus.family.order for o in world] -[] ->>> len(db.connection.queries) -2 - ->>> db.reset_queries() ->>> Species.objects.all().select_related('genus__family__order').order_by('id')[0:1].get().genus.family.order.name -u'Diptera' ->>> len(db.connection.queries) -1 - -# Specifying both "depth" and fields is an error. ->>> Species.objects.select_related('genus__family__order', depth=4) -Traceback (most recent call last): -... -TypeError: Cannot pass both "depth" and fields to select_related() - -# Reset DEBUG to where we found it. ->>> settings.DEBUG = False -"""} - + return self.name \ No newline at end of file diff --git a/tests/modeltests/select_related/tests.py b/tests/modeltests/select_related/tests.py new file mode 100644 index 000000000000..72b3ab25e986 --- /dev/null +++ b/tests/modeltests/select_related/tests.py @@ -0,0 +1,166 @@ +from django.test import TestCase +from django.conf import settings +from django import db + +from models import Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species + +class SelectRelatedTests(TestCase): + + def create_tree(self, stringtree): + """ + Helper to create a complete tree. + """ + names = stringtree.split() + models = [Domain, Kingdom, Phylum, Klass, Order, Family, Genus, Species] + assert len(names) == len(models), (names, models) + + parent = None + for name, model in zip(names, models): + try: + obj = model.objects.get(name=name) + except model.DoesNotExist: + obj = model(name=name) + if parent: + setattr(obj, parent.__class__.__name__.lower(), parent) + obj.save() + parent = obj + + def create_base_data(self): + self.create_tree("Eukaryota Animalia Anthropoda Insecta Diptera Drosophilidae Drosophila melanogaster") + self.create_tree("Eukaryota Animalia Chordata Mammalia Primates Hominidae Homo sapiens") + self.create_tree("Eukaryota Plantae Magnoliophyta Magnoliopsida Fabales Fabaceae Pisum sativum") + self.create_tree("Eukaryota Fungi Basidiomycota Homobasidiomycatae Agaricales Amanitacae Amanita muscaria") + + def setUp(self): + # The test runner sets settings.DEBUG to False, but we want to gather + # queries so we'll set it to True here and reset it at the end of the + # test case. + self.create_base_data() + settings.DEBUG = True + db.reset_queries() + + def tearDown(self): + settings.DEBUG = False + + def test_access_fks_without_select_related(self): + """ + Normally, accessing FKs doesn't fill in related objects + """ + fly = Species.objects.get(name="melanogaster") + domain = fly.genus.family.order.klass.phylum.kingdom.domain + self.assertEqual(domain.name, 'Eukaryota') + self.assertEqual(len(db.connection.queries), 8) + + def test_access_fks_with_select_related(self): + """ + A select_related() call will fill in those related objects without any + extra queries + """ + person = Species.objects.select_related(depth=10).get(name="sapiens") + domain = person.genus.family.order.klass.phylum.kingdom.domain + self.assertEqual(domain.name, 'Eukaryota') + self.assertEqual(len(db.connection.queries), 1) + + def test_list_without_select_related(self): + """ + select_related() also of course applies to entire lists, not just + items. This test verifies the expected behavior without select_related. + """ + world = Species.objects.all() + families = [o.genus.family.name for o in world] + self.assertEqual(sorted(families), [ + 'Amanitacae', + 'Drosophilidae', + 'Fabaceae', + 'Hominidae', + ]) + self.assertEqual(len(db.connection.queries), 9) + + def test_list_with_select_related(self): + """ + select_related() also of course applies to entire lists, not just + items. This test verifies the expected behavior with select_related. + """ + world = Species.objects.all().select_related() + families = [o.genus.family.name for o in world] + self.assertEqual(sorted(families), [ + 'Amanitacae', + 'Drosophilidae', + 'Fabaceae', + 'Hominidae', + ]) + self.assertEqual(len(db.connection.queries), 1) + + def test_depth(self, depth=1, expected=7): + """ + The "depth" argument to select_related() will stop the descent at a + particular level. + """ + pea = Species.objects.select_related(depth=depth).get(name="sativum") + self.assertEqual( + pea.genus.family.order.klass.phylum.kingdom.domain.name, + 'Eukaryota' + ) + # Notice: one fewer queries than above because of depth=1 + self.assertEqual(len(db.connection.queries), expected) + + def test_larger_depth(self): + """ + The "depth" argument to select_related() will stop the descent at a + particular level. This tests a larger depth value. + """ + self.test_depth(depth=5, expected=3) + + def test_list_with_depth(self): + """ + The "depth" argument to select_related() will stop the descent at a + particular level. This can be used on lists as well. + """ + world = Species.objects.all().select_related(depth=2) + orders = [o.genus.family.order.name for o in world] + self.assertEqual(sorted(orders), + ['Agaricales', 'Diptera', 'Fabales', 'Primates']) + self.assertEqual(len(db.connection.queries), 5) + + def test_select_related_with_extra(self): + s = Species.objects.all().select_related(depth=1)\ + .extra(select={'a': 'select_related_species.id + 10'})[0] + self.assertEqual(s.id + 10, s.a) + + def test_certain_fields(self): + """ + The optional fields passed to select_related() control which related + models we pull in. This allows for smaller queries and can act as an + alternative (or, in addition to) the depth parameter. + + In this case, we explicitly say to select the 'genus' and + 'genus.family' models, leading to the same number of queries as before. + """ + world = Species.objects.select_related('genus__family') + families = [o.genus.family.name for o in world] + self.assertEqual(sorted(families), + ['Amanitacae', 'Drosophilidae', 'Fabaceae', 'Hominidae']) + self.assertEqual(len(db.connection.queries), 1) + + def test_more_certain_fields(self): + """ + In this case, we explicitly say to select the 'genus' and + 'genus.family' models, leading to the same number of queries as before. + """ + world = Species.objects.filter(genus__name='Amanita')\ + .select_related('genus__family') + orders = [o.genus.family.order.name for o in world] + self.assertEqual(orders, [u'Agaricales']) + self.assertEqual(len(db.connection.queries), 2) + + def test_field_traversal(self): + s = Species.objects.all().select_related('genus__family__order' + ).order_by('id')[0:1].get().genus.family.order.name + self.assertEqual(s, u'Diptera') + self.assertEqual(len(db.connection.queries), 1) + + def test_depth_fields_fails(self): + self.assertRaises(TypeError, + Species.objects.select_related, + 'genus__family__order', depth=4 + ) diff --git a/tests/modeltests/serializers/models.py b/tests/modeltests/serializers/models.py index aac945077ba2..c12e73fd6e73 100644 --- a/tests/modeltests/serializers/models.py +++ b/tests/modeltests/serializers/models.py @@ -18,6 +18,7 @@ class Meta: def __unicode__(self): return self.name + class Author(models.Model): name = models.CharField(max_length=20) @@ -27,6 +28,7 @@ class Meta: def __unicode__(self): return self.name + class Article(models.Model): author = models.ForeignKey(Author) headline = models.CharField(max_length=50) @@ -39,6 +41,7 @@ class Meta: def __unicode__(self): return self.headline + class AuthorProfile(models.Model): author = models.OneToOneField(Author, primary_key=True) date_of_birth = models.DateField() @@ -46,6 +49,7 @@ class AuthorProfile(models.Model): def __unicode__(self): return u"Profile of %s" % self.author + class Actor(models.Model): name = models.CharField(max_length=20, primary_key=True) @@ -55,6 +59,7 @@ class Meta: def __unicode__(self): return self.name + class Movie(models.Model): actor = models.ForeignKey(Actor) title = models.CharField(max_length=50) @@ -66,6 +71,7 @@ class Meta: def __unicode__(self): return self.title + class Score(models.Model): score = models.FloatField() @@ -83,6 +89,7 @@ def __str__(self): def to_string(self): return "%s" % self.title + class TeamField(models.CharField): __metaclass__ = models.SubfieldBase @@ -100,6 +107,7 @@ def to_python(self, value): def value_to_string(self, obj): return self._get_val_from_obj(obj).to_string() + class Player(models.Model): name = models.CharField(max_length=50) rank = models.IntegerField() @@ -107,238 +115,3 @@ class Player(models.Model): def __unicode__(self): return u'%s (%d) playing for %s' % (self.name, self.rank, self.team.to_string()) - -__test__ = {'API_TESTS':""" -# Create some data: ->>> from datetime import datetime ->>> sports = Category(name="Sports") ->>> music = Category(name="Music") ->>> op_ed = Category(name="Op-Ed") ->>> sports.save(); music.save(); op_ed.save() - ->>> joe = Author(name="Joe") ->>> jane = Author(name="Jane") ->>> joe.save(); jane.save() - ->>> a1 = Article( -... author = jane, -... headline = "Poker has no place on ESPN", -... pub_date = datetime(2006, 6, 16, 11, 00)) ->>> a2 = Article( -... author = joe, -... headline = "Time to reform copyright", -... pub_date = datetime(2006, 6, 16, 13, 00, 11, 345)) ->>> a1.save(); a2.save() ->>> a1.categories = [sports, op_ed] ->>> a2.categories = [music, op_ed] - -# Serialize a queryset to XML ->>> from django.core import serializers ->>> xml = serializers.serialize("xml", Article.objects.all()) - -# The output is valid XML ->>> from xml.dom import minidom ->>> dom = minidom.parseString(xml) - -# Deserializing has a similar interface, except that special DeserializedObject -# instances are returned. This is because data might have changed in the -# database since the data was serialized (we'll simulate that below). ->>> for obj in serializers.deserialize("xml", xml): -... print obj - - - -# Deserializing data with different field values doesn't change anything in the -# database until we call save(): ->>> xml = xml.replace("Poker has no place on ESPN", "Poker has no place on television") ->>> objs = list(serializers.deserialize("xml", xml)) - -# Even those I deserialized, the database hasn't been touched ->>> Article.objects.all() -[, ] - -# But when I save, the data changes as you might except. ->>> objs[0].save() ->>> Article.objects.all() -[, ] - -# Django also ships with a built-in JSON serializers ->>> json = serializers.serialize("json", Category.objects.filter(pk=2)) ->>> json -'[{"pk": 2, "model": "serializers.category", "fields": {"name": "Music"}}]' - -# You can easily create new objects by deserializing data with an empty PK -# (It's easier to demo this with JSON...) ->>> new_author_json = '[{"pk": null, "model": "serializers.author", "fields": {"name": "Bill"}}]' ->>> for obj in serializers.deserialize("json", new_author_json): -... obj.save() ->>> Author.objects.all() -[, , ] - -# All the serializers work the same ->>> json = serializers.serialize("json", Article.objects.all()) ->>> for obj in serializers.deserialize("json", json): -... print obj - - - ->>> json = json.replace("Poker has no place on television", "Just kidding; I love TV poker") ->>> for obj in serializers.deserialize("json", json): -... obj.save() - ->>> Article.objects.all() -[, ] - -# If you use your own primary key field (such as a OneToOneField), -# it doesn't appear in the serialized field list - it replaces the -# pk identifier. ->>> profile = AuthorProfile(author=joe, date_of_birth=datetime(1970,1,1)) ->>> profile.save() - ->>> json = serializers.serialize("json", AuthorProfile.objects.all()) ->>> json -'[{"pk": 1, "model": "serializers.authorprofile", "fields": {"date_of_birth": "1970-01-01"}}]' - ->>> for obj in serializers.deserialize("json", json): -... print obj - - -# Objects ids can be referenced before they are defined in the serialization data -# However, the deserialization process will need to be contained within a transaction ->>> json = '[{"pk": 3, "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00", "categories": [4, 1], "author": 4}}, {"pk": 4, "model": "serializers.category", "fields": {"name": "Reference"}}, {"pk": 4, "model": "serializers.author", "fields": {"name": "Agnes"}}]' ->>> from django.db import transaction ->>> transaction.enter_transaction_management() ->>> transaction.managed(True) ->>> for obj in serializers.deserialize("json", json): -... obj.save() - ->>> transaction.commit() ->>> transaction.leave_transaction_management() - ->>> article = Article.objects.get(pk=3) ->>> article - ->>> article.categories.all() -[, ] ->>> article.author - - -# Serializer output can be restricted to a subset of fields ->>> print serializers.serialize("json", Article.objects.all(), fields=('headline','pub_date')) -[{"pk": 1, "model": "serializers.article", "fields": {"headline": "Just kidding; I love TV poker", "pub_date": "2006-06-16 11:00:00"}}, {"pk": 2, "model": "serializers.article", "fields": {"headline": "Time to reform copyright", "pub_date": "2006-06-16 13:00:11"}}, {"pk": 3, "model": "serializers.article", "fields": {"headline": "Forward references pose no problem", "pub_date": "2006-06-16 15:00:00"}}] - -# Every string is serialized as a unicode object, also primary key -# which is 'varchar' ->>> ac = Actor(name="Zażółć") ->>> mv = Movie(title="Gęślą jaźń", actor=ac) ->>> ac.save(); mv.save() - -# Let's serialize our movie ->>> print serializers.serialize("json", [mv]) -[{"pk": 1, "model": "serializers.movie", "fields": {"price": "0.00", "actor": "Za\u017c\u00f3\u0142\u0107", "title": "G\u0119\u015bl\u0105 ja\u017a\u0144"}}] - -# Deserialization of movie ->>> list(serializers.deserialize('json', serializers.serialize('json', [mv])))[0].object.title -u'G\u0119\u015bl\u0105 ja\u017a\u0144' - -# None is null after serialization to json -# Primary key is None in case of not saved model ->>> mv2 = Movie(title="Movie 2", actor=ac) ->>> print serializers.serialize("json", [mv2]) -[{"pk": null, "model": "serializers.movie", "fields": {"price": "0.00", "actor": "Za\u017c\u00f3\u0142\u0107", "title": "Movie 2"}}] - -# Deserialization of null returns None for pk ->>> print list(serializers.deserialize('json', serializers.serialize('json', [mv2])))[0].object.id -None - -# Serialization and deserialization of floats: ->>> sc = Score(score=3.4) ->>> print serializers.serialize("json", [sc]) -[{"pk": null, "model": "serializers.score", "fields": {"score": 3.4}}] ->>> print list(serializers.deserialize('json', serializers.serialize('json', [sc])))[0].object.score -3.4 - -# Custom field with non trivial to string convertion value ->>> player = Player() ->>> player.name = "Soslan Djanaev" ->>> player.rank = 1 ->>> player.team = Team("Spartak Moskva") ->>> player.save() - ->>> serialized = serializers.serialize("json", Player.objects.all()) ->>> print serialized -[{"pk": 1, "model": "serializers.player", "fields": {"name": "Soslan Djanaev", "rank": 1, "team": "Spartak Moskva"}}] - ->>> obj = list(serializers.deserialize("json", serialized))[0] ->>> print obj - - -# Regression for #12524 -- dates before 1000AD get prefixed 0's on the year ->>> a = Article.objects.create( -... pk=4, -... author = jane, -... headline = "Nobody remembers the early years", -... pub_date = datetime(1, 2, 3, 4, 5, 6)) - ->>> serialized = serializers.serialize("json", [a]) ->>> print serialized -[{"pk": 4, "model": "serializers.article", "fields": {"headline": "Nobody remembers the early years", "pub_date": "0001-02-03 04:05:06", "categories": [], "author": 2}}] - ->>> obj = list(serializers.deserialize("json", serialized))[0] ->>> print obj.object.pub_date -0001-02-03 04:05:06 - -"""} - -try: - import yaml - __test__['YAML'] = """ -# Create some data: - ->>> articles = Article.objects.all().order_by("id")[:2] ->>> from django.core import serializers - -# test if serial - ->>> serialized = serializers.serialize("yaml", articles) ->>> print serialized -- fields: - author: 2 - categories: [3, 1] - headline: Just kidding; I love TV poker - pub_date: 2006-06-16 11:00:00 - model: serializers.article - pk: 1 -- fields: - author: 1 - categories: [2, 3] - headline: Time to reform copyright - pub_date: 2006-06-16 13:00:11 - model: serializers.article - pk: 2 - - ->>> obs = list(serializers.deserialize("yaml", serialized)) ->>> for i in obs: -... print i - - - -# Custom field with non trivial to string convertion value with YAML serializer - ->>> print serializers.serialize("yaml", Player.objects.all()) -- fields: {name: Soslan Djanaev, rank: 1, team: Spartak Moskva} - model: serializers.player - pk: 1 - - ->>> serialized = serializers.serialize("yaml", Player.objects.all()) ->>> obj = list(serializers.deserialize("yaml", serialized))[0] ->>> print obj - - - -""" -except ImportError: - pass - diff --git a/tests/modeltests/serializers/tests.py b/tests/modeltests/serializers/tests.py new file mode 100644 index 000000000000..0afb344acf6c --- /dev/null +++ b/tests/modeltests/serializers/tests.py @@ -0,0 +1,469 @@ +# -*- coding: utf-8 -*- +from datetime import datetime +from StringIO import StringIO +import unittest +from xml.dom import minidom + +from django.conf import settings +from django.core import serializers +from django.db import transaction +from django.test import TestCase, TransactionTestCase, Approximate +from django.utils import simplejson + +from models import Category, Author, Article, AuthorProfile, Actor, \ + Movie, Score, Player, Team + +class SerializerRegistrationTests(unittest.TestCase): + def setUp(self): + self.old_SERIALIZATION_MODULES = getattr(settings, 'SERIALIZATION_MODULES', None) + self.old_serializers = serializers._serializers + + serializers._serializers = {} + settings.SERIALIZATION_MODULES = { + "json2" : "django.core.serializers.json", + } + + def tearDown(self): + serializers._serializers = self.old_serializers + if self.old_SERIALIZATION_MODULES: + settings.SERIALIZATION_MODULES = self.old_SERIALIZATION_MODULES + else: + delattr(settings, 'SERIALIZATION_MODULES') + + def test_register(self): + "Registering a new serializer populates the full registry. Refs #14823" + serializers.register_serializer('json3', 'django.core.serializers.json') + + public_formats = serializers.get_public_serializer_formats() + self.assertTrue('json3' in public_formats) + self.assertTrue('json2' in public_formats) + self.assertTrue('xml' in public_formats) + + def test_unregister(self): + "Unregistering a serializer doesn't cause the registry to be repopulated. Refs #14823" + serializers.unregister_serializer('xml') + serializers.register_serializer('json3', 'django.core.serializers.json') + + public_formats = serializers.get_public_serializer_formats() + + self.assertFalse('xml' in public_formats) + self.assertTrue('json3' in public_formats) + + def test_builtin_serializers(self): + "Requesting a list of serializer formats popuates the registry" + all_formats = set(serializers.get_serializer_formats()) + public_formats = set(serializers.get_public_serializer_formats()) + + self.assertTrue('xml' in all_formats), + self.assertTrue('xml' in public_formats) + + self.assertTrue('json2' in all_formats) + self.assertTrue('json2' in public_formats) + + self.assertTrue('python' in all_formats) + self.assertFalse('python' in public_formats) + +class SerializersTestBase(object): + @staticmethod + def _comparison_value(value): + return value + + def setUp(self): + sports = Category.objects.create(name="Sports") + music = Category.objects.create(name="Music") + op_ed = Category.objects.create(name="Op-Ed") + + self.joe = Author.objects.create(name="Joe") + self.jane = Author.objects.create(name="Jane") + + self.a1 = Article( + author=self.jane, + headline="Poker has no place on ESPN", + pub_date=datetime(2006, 6, 16, 11, 00) + ) + self.a1.save() + self.a1.categories = [sports, op_ed] + + self.a2 = Article( + author=self.joe, + headline="Time to reform copyright", + pub_date=datetime(2006, 6, 16, 13, 00, 11, 345) + ) + self.a2.save() + self.a2.categories = [music, op_ed] + + def test_serialize(self): + """Tests that basic serialization works.""" + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all()) + self.assertTrue(self._validate_output(serial_str)) + + def test_serializer_roundtrip(self): + """Tests that serialized content can be deserialized.""" + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all()) + models = list(serializers.deserialize(self.serializer_name, serial_str)) + self.assertEqual(len(models), 2) + + def test_altering_serialized_output(self): + """ + Tests the ability to create new objects by + modifying serialized content. + """ + old_headline = "Poker has no place on ESPN" + new_headline = "Poker has no place on television" + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all()) + serial_str = serial_str.replace(old_headline, new_headline) + models = list(serializers.deserialize(self.serializer_name, serial_str)) + + # Prior to saving, old headline is in place + self.assertTrue(Article.objects.filter(headline=old_headline)) + self.assertFalse(Article.objects.filter(headline=new_headline)) + + for model in models: + model.save() + + # After saving, new headline is in place + self.assertTrue(Article.objects.filter(headline=new_headline)) + self.assertFalse(Article.objects.filter(headline=old_headline)) + + def test_one_to_one_as_pk(self): + """ + Tests that if you use your own primary key field + (such as a OneToOneField), it doesn't appear in the + serialized field list - it replaces the pk identifier. + """ + profile = AuthorProfile(author=self.joe, + date_of_birth=datetime(1970,1,1)) + profile.save() + serial_str = serializers.serialize(self.serializer_name, + AuthorProfile.objects.all()) + self.assertFalse(self._get_field_values(serial_str, 'author')) + + for obj in serializers.deserialize(self.serializer_name, serial_str): + self.assertEqual(obj.object.pk, self._comparison_value(self.joe.pk)) + + def test_serialize_field_subset(self): + """Tests that output can be restricted to a subset of fields""" + valid_fields = ('headline','pub_date') + invalid_fields = ("author", "categories") + serial_str = serializers.serialize(self.serializer_name, + Article.objects.all(), + fields=valid_fields) + for field_name in invalid_fields: + self.assertFalse(self._get_field_values(serial_str, field_name)) + + for field_name in valid_fields: + self.assertTrue(self._get_field_values(serial_str, field_name)) + + def test_serialize_unicode(self): + """Tests that unicode makes the roundtrip intact""" + actor_name = u"Za\u017c\u00f3\u0142\u0107" + movie_title = u'G\u0119\u015bl\u0105 ja\u017a\u0144' + ac = Actor(name=actor_name) + mv = Movie(title=movie_title, actor=ac) + ac.save() + mv.save() + + serial_str = serializers.serialize(self.serializer_name, [mv]) + self.assertEqual(self._get_field_values(serial_str, "title")[0], movie_title) + self.assertEqual(self._get_field_values(serial_str, "actor")[0], actor_name) + + obj_list = list(serializers.deserialize(self.serializer_name, serial_str)) + mv_obj = obj_list[0].object + self.assertEqual(mv_obj.title, movie_title) + + def test_serialize_with_null_pk(self): + """ + Tests that serialized data with no primary key results + in a model instance with no id + """ + category = Category(name="Reference") + serial_str = serializers.serialize(self.serializer_name, [category]) + pk_value = self._get_pk_values(serial_str)[0] + self.assertFalse(pk_value) + + cat_obj = list(serializers.deserialize(self.serializer_name, + serial_str))[0].object + self.assertEqual(cat_obj.id, None) + + def test_float_serialization(self): + """Tests that float values serialize and deserialize intact""" + sc = Score(score=3.4) + sc.save() + serial_str = serializers.serialize(self.serializer_name, [sc]) + deserial_objs = list(serializers.deserialize(self.serializer_name, + serial_str)) + self.assertEqual(deserial_objs[0].object.score, Approximate(3.4, places=1)) + + def test_custom_field_serialization(self): + """Tests that custom fields serialize and deserialize intact""" + team_str = "Spartak Moskva" + player = Player() + player.name = "Soslan Djanaev" + player.rank = 1 + player.team = Team(team_str) + player.save() + serial_str = serializers.serialize(self.serializer_name, + Player.objects.all()) + team = self._get_field_values(serial_str, "team") + self.assertTrue(team) + self.assertEqual(team[0], team_str) + + deserial_objs = list(serializers.deserialize(self.serializer_name, serial_str)) + self.assertEqual(deserial_objs[0].object.team.to_string(), + player.team.to_string()) + + def test_pre_1000ad_date(self): + """Tests that year values before 1000AD are properly formatted""" + # Regression for #12524 -- dates before 1000AD get prefixed + # 0's on the year + a = Article.objects.create( + author = self.jane, + headline = "Nobody remembers the early years", + pub_date = datetime(1, 2, 3, 4, 5, 6)) + + serial_str = serializers.serialize(self.serializer_name, [a]) + date_values = self._get_field_values(serial_str, "pub_date") + self.assertEquals(date_values[0], "0001-02-03 04:05:06") + + def test_pkless_serialized_strings(self): + """ + Tests that serialized strings without PKs + can be turned into models + """ + deserial_objs = list(serializers.deserialize(self.serializer_name, + self.pkless_str)) + for obj in deserial_objs: + self.assertFalse(obj.object.id) + obj.save() + self.assertEqual(Category.objects.all().count(), 4) + + +class SerializersTransactionTestBase(object): + def test_forward_refs(self): + """ + Tests that objects ids can be referenced before they are + defined in the serialization data. + """ + # The deserialization process needs to be contained + # within a transaction in order to test forward reference + # handling. + transaction.enter_transaction_management() + transaction.managed(True) + objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str) + for obj in objs: + obj.save() + transaction.commit() + transaction.leave_transaction_management() + + for model_cls in (Category, Author, Article): + self.assertEqual(model_cls.objects.all().count(), 1) + art_obj = Article.objects.all()[0] + self.assertEqual(art_obj.categories.all().count(), 1) + self.assertEqual(art_obj.author.name, "Agnes") + + +class XmlSerializerTestCase(SerializersTestBase, TestCase): + serializer_name = "xml" + pkless_str = """ + + + Reference + +""" + + @staticmethod + def _comparison_value(value): + # The XML serializer handles everything as strings, so comparisons + # need to be performed on the stringified value + return unicode(value) + + @staticmethod + def _validate_output(serial_str): + try: + minidom.parseString(serial_str) + except Exception: + return False + else: + return True + + @staticmethod + def _get_pk_values(serial_str): + ret_list = [] + dom = minidom.parseString(serial_str) + fields = dom.getElementsByTagName("object") + for field in fields: + ret_list.append(field.getAttribute("pk")) + return ret_list + + @staticmethod + def _get_field_values(serial_str, field_name): + ret_list = [] + dom = minidom.parseString(serial_str) + fields = dom.getElementsByTagName("field") + for field in fields: + if field.getAttribute("name") == field_name: + temp = [] + for child in field.childNodes: + temp.append(child.nodeValue) + ret_list.append("".join(temp)) + return ret_list + +class XmlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): + serializer_name = "xml" + fwd_ref_str = """ + + + 1 + Forward references pose no problem + 2006-06-16 15:00:00 + + + + + + Agnes + + + Reference +""" + + +class JsonSerializerTestCase(SerializersTestBase, TestCase): + serializer_name = "json" + pkless_str = """[{"pk": null, "model": "serializers.category", "fields": {"name": "Reference"}}]""" + + @staticmethod + def _validate_output(serial_str): + try: + simplejson.loads(serial_str) + except Exception: + return False + else: + return True + + @staticmethod + def _get_pk_values(serial_str): + ret_list = [] + serial_list = simplejson.loads(serial_str) + for obj_dict in serial_list: + ret_list.append(obj_dict["pk"]) + return ret_list + + @staticmethod + def _get_field_values(serial_str, field_name): + ret_list = [] + serial_list = simplejson.loads(serial_str) + for obj_dict in serial_list: + if field_name in obj_dict["fields"]: + ret_list.append(obj_dict["fields"][field_name]) + return ret_list + +class JsonSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): + serializer_name = "json" + fwd_ref_str = """[ + { + "pk": 1, + "model": "serializers.article", + "fields": { + "headline": "Forward references pose no problem", + "pub_date": "2006-06-16 15:00:00", + "categories": [1], + "author": 1 + } + }, + { + "pk": 1, + "model": "serializers.category", + "fields": { + "name": "Reference" + } + }, + { + "pk": 1, + "model": "serializers.author", + "fields": { + "name": "Agnes" + } + }]""" + +try: + import yaml +except ImportError: + pass +else: + class YamlSerializerTestCase(SerializersTestBase, TestCase): + serializer_name = "yaml" + fwd_ref_str = """- fields: + headline: Forward references pose no problem + pub_date: 2006-06-16 15:00:00 + categories: [1] + author: 1 + pk: 1 + model: serializers.article +- fields: + name: Reference + pk: 1 + model: serializers.category +- fields: + name: Agnes + pk: 1 + model: serializers.author""" + + pkless_str = """- fields: + name: Reference + pk: null + model: serializers.category""" + + @staticmethod + def _validate_output(serial_str): + try: + yaml.load(StringIO(serial_str)) + except Exception: + return False + else: + return True + + @staticmethod + def _get_pk_values(serial_str): + ret_list = [] + stream = StringIO(serial_str) + for obj_dict in yaml.load(stream): + ret_list.append(obj_dict["pk"]) + return ret_list + + @staticmethod + def _get_field_values(serial_str, field_name): + ret_list = [] + stream = StringIO(serial_str) + for obj_dict in yaml.load(stream): + if "fields" in obj_dict and field_name in obj_dict["fields"]: + field_value = obj_dict["fields"][field_name] + # yaml.load will return non-string objects for some + # of the fields we are interested in, this ensures that + # everything comes back as a string + if isinstance(field_value, basestring): + ret_list.append(field_value) + else: + ret_list.append(str(field_value)) + return ret_list + + class YamlSerializerTransactionTestCase(SerializersTransactionTestBase, TransactionTestCase): + serializer_name = "yaml" + fwd_ref_str = """- fields: + headline: Forward references pose no problem + pub_date: 2006-06-16 15:00:00 + categories: [1] + author: 1 + pk: 1 + model: serializers.article +- fields: + name: Reference + pk: 1 + model: serializers.category +- fields: + name: Agnes + pk: 1 + model: serializers.author""" diff --git a/tests/modeltests/signals/models.py b/tests/modeltests/signals/models.py index ea8137f657a8..f1250b42fef1 100644 --- a/tests/modeltests/signals/models.py +++ b/tests/modeltests/signals/models.py @@ -4,114 +4,10 @@ from django.db import models + class Person(models.Model): first_name = models.CharField(max_length=20) last_name = models.CharField(max_length=20) def __unicode__(self): return u"%s %s" % (self.first_name, self.last_name) - -def pre_save_test(signal, sender, instance, **kwargs): - print 'pre_save signal,', instance - if kwargs.get('raw'): - print 'Is raw' - -def post_save_test(signal, sender, instance, **kwargs): - print 'post_save signal,', instance - if 'created' in kwargs: - if kwargs['created']: - print 'Is created' - else: - print 'Is updated' - if kwargs.get('raw'): - print 'Is raw' - -def pre_delete_test(signal, sender, instance, **kwargs): - print 'pre_delete signal,', instance - print 'instance.id is not None: %s' % (instance.id != None) - -# #8285: signals can be any callable -class PostDeleteHandler(object): - def __call__(self, signal, sender, instance, **kwargs): - print 'post_delete signal,', instance - print 'instance.id is None: %s' % (instance.id == None) - -post_delete_test = PostDeleteHandler() - -__test__ = {'API_TESTS':""" - -# Save up the number of connected signals so that we can check at the end -# that all the signals we register get properly unregistered (#9989) ->>> pre_signals = (len(models.signals.pre_save.receivers), -... len(models.signals.post_save.receivers), -... len(models.signals.pre_delete.receivers), -... len(models.signals.post_delete.receivers)) - ->>> models.signals.pre_save.connect(pre_save_test) ->>> models.signals.post_save.connect(post_save_test) ->>> models.signals.pre_delete.connect(pre_delete_test) ->>> models.signals.post_delete.connect(post_delete_test) - ->>> p1 = Person(first_name='John', last_name='Smith') ->>> p1.save() -pre_save signal, John Smith -post_save signal, John Smith -Is created - ->>> p1.first_name = 'Tom' ->>> p1.save() -pre_save signal, Tom Smith -post_save signal, Tom Smith -Is updated - -# Calling an internal method purely so that we can trigger a "raw" save. ->>> p1.save_base(raw=True) -pre_save signal, Tom Smith -Is raw -post_save signal, Tom Smith -Is updated -Is raw - ->>> p1.delete() -pre_delete signal, Tom Smith -instance.id is not None: True -post_delete signal, Tom Smith -instance.id is None: False - ->>> p2 = Person(first_name='James', last_name='Jones') ->>> p2.id = 99999 ->>> p2.save() -pre_save signal, James Jones -post_save signal, James Jones -Is created - ->>> p2.id = 99998 ->>> p2.save() -pre_save signal, James Jones -post_save signal, James Jones -Is created - ->>> p2.delete() -pre_delete signal, James Jones -instance.id is not None: True -post_delete signal, James Jones -instance.id is None: False - ->>> Person.objects.all() -[] - ->>> models.signals.post_delete.disconnect(post_delete_test) ->>> models.signals.pre_delete.disconnect(pre_delete_test) ->>> models.signals.post_save.disconnect(post_save_test) ->>> models.signals.pre_save.disconnect(pre_save_test) - -# Check that all our signals got disconnected properly. ->>> post_signals = (len(models.signals.pre_save.receivers), -... len(models.signals.post_save.receivers), -... len(models.signals.pre_delete.receivers), -... len(models.signals.post_delete.receivers)) - ->>> pre_signals == post_signals -True - -"""} diff --git a/tests/modeltests/signals/tests.py b/tests/modeltests/signals/tests.py index 329636c30665..27948c6d14ec 100644 --- a/tests/modeltests/signals/tests.py +++ b/tests/modeltests/signals/tests.py @@ -1,6 +1,18 @@ from django.db.models import signals from django.test import TestCase -from modeltests.signals.models import Person + +from models import Person + + +# #8285: signals can be any callable +class PostDeleteHandler(object): + def __init__(self, data): + self.data = data + + def __call__(self, signal, sender, instance, **kwargs): + self.data.append( + (instance, instance.id is None) + ) class MyReceiver(object): def __init__(self, param): @@ -12,6 +24,115 @@ def __call__(self, signal, sender, **kwargs): signal.disconnect(receiver=self, sender=sender) class SignalTests(TestCase): + def test_basic(self): + # Save up the number of connected signals so that we can check at the + # end that all the signals we register get properly unregistered (#9989) + pre_signals = ( + len(signals.pre_save.receivers), + len(signals.post_save.receivers), + len(signals.pre_delete.receivers), + len(signals.post_delete.receivers), + ) + + data = [] + + def pre_save_test(signal, sender, instance, **kwargs): + data.append( + (instance, kwargs.get("raw", False)) + ) + signals.pre_save.connect(pre_save_test) + + def post_save_test(signal, sender, instance, **kwargs): + data.append( + (instance, kwargs.get("created"), kwargs.get("raw", False)) + ) + signals.post_save.connect(post_save_test) + + def pre_delete_test(signal, sender, instance, **kwargs): + data.append( + (instance, instance.id is None) + ) + signals.pre_delete.connect(pre_delete_test) + + post_delete_test = PostDeleteHandler(data) + signals.post_delete.connect(post_delete_test) + + p1 = Person(first_name="John", last_name="Smith") + self.assertEqual(data, []) + p1.save() + self.assertEqual(data, [ + (p1, False), + (p1, True, False), + ]) + data[:] = [] + + p1.first_name = "Tom" + p1.save() + self.assertEqual(data, [ + (p1, False), + (p1, False, False), + ]) + data[:] = [] + + # Calling an internal method purely so that we can trigger a "raw" save. + p1.save_base(raw=True) + self.assertEqual(data, [ + (p1, True), + (p1, False, True), + ]) + data[:] = [] + + p1.delete() + self.assertEqual(data, [ + (p1, False), + (p1, False), + ]) + data[:] = [] + + p2 = Person(first_name="James", last_name="Jones") + p2.id = 99999 + p2.save() + self.assertEqual(data, [ + (p2, False), + (p2, True, False), + ]) + data[:] = [] + + p2.id = 99998 + p2.save() + self.assertEqual(data, [ + (p2, False), + (p2, True, False), + ]) + data[:] = [] + + p2.delete() + self.assertEqual(data, [ + (p2, False), + (p2, False) + ]) + + self.assertQuerysetEqual( + Person.objects.all(), [ + "James Jones", + ], + unicode + ) + + signals.post_delete.disconnect(post_delete_test) + signals.pre_delete.disconnect(pre_delete_test) + signals.post_save.disconnect(post_save_test) + signals.pre_save.disconnect(pre_save_test) + + # Check that all our signals got disconnected properly. + post_signals = ( + len(signals.pre_save.receivers), + len(signals.post_save.receivers), + len(signals.pre_delete.receivers), + len(signals.post_delete.receivers), + ) + self.assertEqual(pre_signals, post_signals) + def test_disconnect_in_dispatch(self): """ Test that signals that disconnect when being called don't mess future @@ -21,8 +142,7 @@ def test_disconnect_in_dispatch(self): signals.post_save.connect(sender=Person, receiver=a) signals.post_save.connect(sender=Person, receiver=b) p = Person.objects.create(first_name='John', last_name='Smith') - - self.failUnless(a._run) - self.failUnless(b._run) + + self.assertTrue(a._run) + self.assertTrue(b._run) self.assertEqual(signals.post_save.receivers, []) - diff --git a/tests/modeltests/str/models.py b/tests/modeltests/str/models.py index 644c6025ab19..84b8d67d12cd 100644 --- a/tests/modeltests/str/models.py +++ b/tests/modeltests/str/models.py @@ -30,23 +30,4 @@ class InternationalArticle(models.Model): pub_date = models.DateTimeField() def __unicode__(self): - return self.headline - -__test__ = {'API_TESTS':ur""" -# Create an Article. ->>> from datetime import datetime ->>> a = Article(headline='Area man programs in Python', pub_date=datetime(2005, 7, 28)) ->>> a.save() - ->>> str(a) -'Area man programs in Python' - ->>> a - - ->>> a1 = InternationalArticle(headline=u'Girl wins €12.500 in lottery', pub_date=datetime(2005, 7, 28)) - -# The default str() output will be the UTF-8 encoded output of __unicode__(). ->>> str(a1) -'Girl wins \xe2\x82\xac12.500 in lottery' -"""} + return self.headline \ No newline at end of file diff --git a/tests/modeltests/str/tests.py b/tests/modeltests/str/tests.py new file mode 100644 index 000000000000..4e4c76501fed --- /dev/null +++ b/tests/modeltests/str/tests.py @@ -0,0 +1,23 @@ + # -*- coding: utf-8 -*- +import datetime + +from django.test import TestCase + +from models import Article, InternationalArticle + +class SimpleTests(TestCase): + def test_basic(self): + a = Article.objects.create( + headline='Area man programs in Python', + pub_date=datetime.datetime(2005, 7, 28) + ) + self.assertEqual(str(a), 'Area man programs in Python') + self.assertEqual(repr(a), '') + + def test_international(self): + a = InternationalArticle.objects.create( + headline=u'Girl wins €12.500 in lottery', + pub_date=datetime.datetime(2005, 7, 28) + ) + # The default str() output will be the UTF-8 encoded output of __unicode__(). + self.assertEqual(str(a), 'Girl wins \xe2\x82\xac12.500 in lottery') \ No newline at end of file diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py index c51323d843cf..82fb70f03890 100644 --- a/tests/modeltests/test_client/models.py +++ b/tests/modeltests/test_client/models.py @@ -21,6 +21,7 @@ """ from django.test import Client, TestCase +from django.conf import settings from django.core import mail class ClientTest(TestCase): @@ -69,7 +70,7 @@ def test_post(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.context['data'], '37') self.assertEqual(response.template.name, 'POST Template') - self.failUnless('Data received' in response.content) + self.assertTrue('Data received' in response.content) def test_response_headers(self): "Check the value of HTTP headers returned in a response" @@ -265,6 +266,13 @@ def test_unknown_page(self): # Check that the response was a 404 self.assertEqual(response.status_code, 404) + def test_url_parameters(self): + "Make sure that URL ;-parameters are not stripped." + response = self.client.get('/test_client/unknown_view/;some-parameter') + + # Check that the path in the response includes it (ignore that it's a 404) + self.assertEqual(response.request['PATH_INFO'], '/test_client/unknown_view/;some-parameter') + def test_view_with_login(self): "Request a page that is protected with @login_required" @@ -274,7 +282,7 @@ def test_view_with_login(self): # Log in login = self.client.login(username='testclient', password='password') - self.failUnless(login, 'Could not log in') + self.assertTrue(login, 'Could not log in') # Request a page that requires a login response = self.client.get('/test_client/login_protected_view/') @@ -290,7 +298,7 @@ def test_view_with_method_login(self): # Log in login = self.client.login(username='testclient', password='password') - self.failUnless(login, 'Could not log in') + self.assertTrue(login, 'Could not log in') # Request a page that requires a login response = self.client.get('/test_client/login_protected_method_view/') @@ -306,7 +314,7 @@ def test_view_with_login_and_custom_redirect(self): # Log in login = self.client.login(username='testclient', password='password') - self.failUnless(login, 'Could not log in') + self.assertTrue(login, 'Could not log in') # Request a page that requires a login response = self.client.get('/test_client/login_protected_view_custom_redirect/') @@ -317,13 +325,13 @@ def test_view_with_bad_login(self): "Request a page that is protected with @login, but use bad credentials" login = self.client.login(username='otheruser', password='nopassword') - self.failIf(login) + self.assertFalse(login) def test_view_with_inactive_login(self): "Request a page that is protected with @login, but use an inactive login" login = self.client.login(username='inactive', password='password') - self.failIf(login) + self.assertFalse(login) def test_logout(self): "Request a logout after logging in" @@ -351,7 +359,7 @@ def test_view_with_permissions(self): # Log in login = self.client.login(username='testclient', password='password') - self.failUnless(login, 'Could not log in') + self.assertTrue(login, 'Could not log in') # Log in with wrong permissions. Should result in 302. response = self.client.get('/test_client/permission_protected_view/') @@ -368,7 +376,7 @@ def test_view_with_method_permissions(self): # Log in login = self.client.login(username='testclient', password='password') - self.failUnless(login, 'Could not log in') + self.assertTrue(login, 'Could not log in') # Log in with wrong permissions. Should result in 302. response = self.client.get('/test_client/permission_protected_method_view/') @@ -433,3 +441,26 @@ def test_mass_mail_sending(self): self.assertEqual(mail.outbox[1].from_email, 'from@example.com') self.assertEqual(mail.outbox[1].to[0], 'second@example.com') self.assertEqual(mail.outbox[1].to[1], 'third@example.com') + +class CSRFEnabledClientTests(TestCase): + def setUp(self): + # Enable the CSRF middleware for this test + self.old_MIDDLEWARE_CLASSES = settings.MIDDLEWARE_CLASSES + csrf_middleware_class = 'django.middleware.csrf.CsrfViewMiddleware' + if csrf_middleware_class not in settings.MIDDLEWARE_CLASSES: + settings.MIDDLEWARE_CLASSES += (csrf_middleware_class,) + + def tearDown(self): + settings.MIDDLEWARE_CLASSES = self.old_MIDDLEWARE_CLASSES + + def test_csrf_enabled_client(self): + "A client can be instantiated with CSRF checks enabled" + csrf_client = Client(enforce_csrf_checks=True) + + # The normal client allows the post + response = self.client.post('/test_client/post_view/', {}) + self.assertEqual(response.status_code, 200) + + # The CSRF-enabled client rejects it + response = csrf_client.post('/test_client/post_view/', {}) + self.assertEqual(response.status_code, 403) diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py index c42a7b7e679e..baa9525b3732 100644 --- a/tests/modeltests/test_client/views.py +++ b/tests/modeltests/test_client/views.py @@ -1,6 +1,6 @@ from xml.dom.minidom import parseString -from django.core.mail import EmailMessage, SMTPConnection +from django.core import mail from django.template import Context, Template from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound from django.contrib.auth.decorators import login_required, permission_required @@ -38,7 +38,7 @@ def view_with_header(request): response = HttpResponse() response['X-DJANGO-TEST'] = 'Slartibartfast' return response - + def raw_post_view(request): """A view which expects raw XML to be posted and returns content extracted from the XML""" @@ -139,7 +139,7 @@ def login_protected_view_changed_redirect(request): "A simple view that is login protected with a custom redirect field set" t = Template('This is a login protected test. Username is {{ user.username }}.', name='Login Template') c = Context({'user': request.user}) - + return HttpResponse(t.render(c)) login_protected_view_changed_redirect = login_required(redirect_field_name="redirect_to")(login_protected_view_changed_redirect) @@ -189,7 +189,7 @@ def broken_view(request): raise KeyError("Oops! Looks like you wrote some bad code.") def mail_sending_view(request): - EmailMessage( + mail.EmailMessage( "Test message", "This is a test email", "from@example.com", @@ -197,18 +197,18 @@ def mail_sending_view(request): return HttpResponse("Mail sent") def mass_mail_sending_view(request): - m1 = EmailMessage( + m1 = mail.EmailMessage( 'First Test message', 'This is the first test email', 'from@example.com', ['first@example.com', 'second@example.com']) - m2 = EmailMessage( + m2 = mail.EmailMessage( 'Second Test message', 'This is the second test email', 'from@example.com', ['second@example.com', 'third@example.com']) - c = SMTPConnection() + c = mail.get_connection() c.send_messages([m1,m2]) return HttpResponse("Mail sent") diff --git a/tests/modeltests/transactions/models.py b/tests/modeltests/transactions/models.py index df0dd805a000..d957fe174ce0 100644 --- a/tests/modeltests/transactions/models.py +++ b/tests/modeltests/transactions/models.py @@ -18,138 +18,4 @@ class Meta: ordering = ('first_name', 'last_name') def __unicode__(self): - return u"%s %s" % (self.first_name, self.last_name) - -__test__ = {'API_TESTS':""" ->>> from django.db import connection, transaction -"""} - -from django.conf import settings - -building_docs = getattr(settings, 'BUILDING_DOCS', False) - -if building_docs or settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.mysql': - __test__['API_TESTS'] += """ -# the default behavior is to autocommit after each save() action ->>> def create_a_reporter_then_fail(first, last): -... a = Reporter(first_name=first, last_name=last) -... a.save() -... raise Exception("I meant to do that") -... ->>> create_a_reporter_then_fail("Alice", "Smith") -Traceback (most recent call last): - ... -Exception: I meant to do that - -# The object created before the exception still exists ->>> Reporter.objects.all() -[] - -# the autocommit decorator works exactly the same as the default behavior ->>> autocomitted_create_then_fail = transaction.autocommit(create_a_reporter_then_fail) ->>> autocomitted_create_then_fail("Ben", "Jones") -Traceback (most recent call last): - ... -Exception: I meant to do that - -# Same behavior as before ->>> Reporter.objects.all() -[, ] - -# the autocommit decorator also works with a using argument ->>> using_autocomitted_create_then_fail = transaction.autocommit(using='default')(create_a_reporter_then_fail) ->>> using_autocomitted_create_then_fail("Carol", "Doe") -Traceback (most recent call last): - ... -Exception: I meant to do that - -# Same behavior as before ->>> Reporter.objects.all() -[, , ] - -# With the commit_on_success decorator, the transaction is only committed if the -# function doesn't throw an exception ->>> committed_on_success = transaction.commit_on_success(create_a_reporter_then_fail) ->>> committed_on_success("Dirk", "Gently") -Traceback (most recent call last): - ... -Exception: I meant to do that - -# This time the object never got saved ->>> Reporter.objects.all() -[, , ] - -# commit_on_success decorator also works with a using argument ->>> using_committed_on_success = transaction.commit_on_success(using='default')(create_a_reporter_then_fail) ->>> using_committed_on_success("Dirk", "Gently") -Traceback (most recent call last): - ... -Exception: I meant to do that - -# This time the object never got saved ->>> Reporter.objects.all() -[, , ] - -# If there aren't any exceptions, the data will get saved ->>> def remove_a_reporter(): -... r = Reporter.objects.get(first_name="Alice") -... r.delete() -... ->>> remove_comitted_on_success = transaction.commit_on_success(remove_a_reporter) ->>> remove_comitted_on_success() ->>> Reporter.objects.all() -[, ] - -# You can manually manage transactions if you really want to, but you -# have to remember to commit/rollback ->>> def manually_managed(): -... r = Reporter(first_name="Dirk", last_name="Gently") -... r.save() -... transaction.commit() ->>> manually_managed = transaction.commit_manually(manually_managed) ->>> manually_managed() ->>> Reporter.objects.all() -[, , ] - -# If you forget, you'll get bad errors ->>> def manually_managed_mistake(): -... r = Reporter(first_name="Edward", last_name="Woodward") -... r.save() -... # oops, I forgot to commit/rollback! ->>> manually_managed_mistake = transaction.commit_manually(manually_managed_mistake) ->>> manually_managed_mistake() -Traceback (most recent call last): - ... -TransactionManagementError: Transaction managed block ended with pending COMMIT/ROLLBACK - -# commit_manually also works with a using argument ->>> using_manually_managed_mistake = transaction.commit_manually(using='default')(manually_managed_mistake) ->>> using_manually_managed_mistake() -Traceback (most recent call last): - ... -TransactionManagementError: Transaction managed block ended with pending COMMIT/ROLLBACK - -""" - -# Regression for #11900: If a function wrapped by commit_on_success writes a -# transaction that can't be committed, that transaction should be rolled back. -# The bug is only visible using the psycopg2 backend, though -# the fix is generally a good idea. -pgsql_backends = ('django.db.backends.postgresql_psycopg2', 'postgresql_psycopg2',) -if building_docs or settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] in pgsql_backends: - __test__['API_TESTS'] += """ ->>> def execute_bad_sql(): -... cursor = connection.cursor() -... cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") -... transaction.set_dirty() -... ->>> execute_bad_sql = transaction.commit_on_success(execute_bad_sql) ->>> execute_bad_sql() -Traceback (most recent call last): - ... -IntegrityError: null value in column "email" violates not-null constraint - - ->>> transaction.rollback() - -""" + return u"%s %s" % (self.first_name, self.last_name) \ No newline at end of file diff --git a/tests/modeltests/transactions/tests.py b/tests/modeltests/transactions/tests.py new file mode 100644 index 000000000000..9964f5d7aba4 --- /dev/null +++ b/tests/modeltests/transactions/tests.py @@ -0,0 +1,155 @@ +from django.test import TransactionTestCase +from django.db import connection, transaction, IntegrityError, DEFAULT_DB_ALIAS +from django.conf import settings + +from models import Reporter + +PGSQL = 'psycopg2' in settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] +MYSQL = 'mysql' in settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] + +class TransactionTests(TransactionTestCase): + + if not MYSQL: + + def create_a_reporter_then_fail(self, first, last): + a = Reporter(first_name=first, last_name=last) + a.save() + raise Exception("I meant to do that") + + def remove_a_reporter(self, first_name): + r = Reporter.objects.get(first_name="Alice") + r.delete() + + def manually_managed(self): + r = Reporter(first_name="Dirk", last_name="Gently") + r.save() + transaction.commit() + + def manually_managed_mistake(self): + r = Reporter(first_name="Edward", last_name="Woodward") + r.save() + # Oops, I forgot to commit/rollback! + + def execute_bad_sql(self): + cursor = connection.cursor() + cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") + transaction.set_dirty() + + def test_autocommit(self): + """ + The default behavior is to autocommit after each save() action. + """ + self.assertRaises(Exception, + self.create_a_reporter_then_fail, + "Alice", "Smith" + ) + + # The object created before the exception still exists + self.assertEqual(Reporter.objects.count(), 1) + + def test_autocommit_decorator(self): + """ + The autocommit decorator works exactly the same as the default behavior. + """ + autocomitted_create_then_fail = transaction.autocommit( + self.create_a_reporter_then_fail + ) + self.assertRaises(Exception, + autocomitted_create_then_fail, + "Alice", "Smith" + ) + # Again, the object created before the exception still exists + self.assertEqual(Reporter.objects.count(), 1) + + def test_autocommit_decorator_with_using(self): + """ + The autocommit decorator also works with a using argument. + """ + autocomitted_create_then_fail = transaction.autocommit(using='default')( + self.create_a_reporter_then_fail + ) + self.assertRaises(Exception, + autocomitted_create_then_fail, + "Alice", "Smith" + ) + # Again, the object created before the exception still exists + self.assertEqual(Reporter.objects.count(), 1) + + def test_commit_on_success(self): + """ + With the commit_on_success decorator, the transaction is only committed + if the function doesn't throw an exception. + """ + committed_on_success = transaction.commit_on_success( + self.create_a_reporter_then_fail) + self.assertRaises(Exception, committed_on_success, "Dirk", "Gently") + # This time the object never got saved + self.assertEqual(Reporter.objects.count(), 0) + + def test_commit_on_success_with_using(self): + """ + The commit_on_success decorator also works with a using argument. + """ + using_committed_on_success = transaction.commit_on_success(using='default')( + self.create_a_reporter_then_fail + ) + self.assertRaises(Exception, + using_committed_on_success, + "Dirk", "Gently" + ) + # This time the object never got saved + self.assertEqual(Reporter.objects.count(), 0) + + def test_commit_on_success_succeed(self): + """ + If there aren't any exceptions, the data will get saved. + """ + Reporter.objects.create(first_name="Alice", last_name="Smith") + remove_comitted_on_success = transaction.commit_on_success( + self.remove_a_reporter + ) + remove_comitted_on_success("Alice") + self.assertEqual(list(Reporter.objects.all()), []) + + def test_manually_managed(self): + """ + You can manually manage transactions if you really want to, but you + have to remember to commit/rollback. + """ + manually_managed = transaction.commit_manually(self.manually_managed) + manually_managed() + self.assertEqual(Reporter.objects.count(), 1) + + def test_manually_managed_mistake(self): + """ + If you forget, you'll get bad errors. + """ + manually_managed_mistake = transaction.commit_manually( + self.manually_managed_mistake + ) + self.assertRaises(transaction.TransactionManagementError, + manually_managed_mistake) + + def test_manually_managed_with_using(self): + """ + The commit_manually function also works with a using argument. + """ + using_manually_managed_mistake = transaction.commit_manually(using='default')( + self.manually_managed_mistake + ) + self.assertRaises(transaction.TransactionManagementError, + using_manually_managed_mistake + ) + + if PGSQL: + + def test_bad_sql(self): + """ + Regression for #11900: If a function wrapped by commit_on_success + writes a transaction that can't be committed, that transaction should + be rolled back. The bug is only visible using the psycopg2 backend, + though the fix is generally a good idea. + """ + execute_bad_sql = transaction.commit_on_success(self.execute_bad_sql) + self.assertRaises(IntegrityError, execute_bad_sql) + transaction.rollback() diff --git a/tests/modeltests/unmanaged_models/models.py b/tests/modeltests/unmanaged_models/models.py index ca9b05aca40d..0c2cf500dae9 100644 --- a/tests/modeltests/unmanaged_models/models.py +++ b/tests/modeltests/unmanaged_models/models.py @@ -123,30 +123,3 @@ class Meta: # table *will* be created (unless given a custom `through` as for C02 above). class Managed1(models.Model): mm = models.ManyToManyField(Unmanaged1) - -__test__ = {'API_TESTS':""" -The main test here is that the all the models can be created without any -database errors. We can also do some more simple insertion and lookup tests -whilst we're here to show that the second of models do refer to the tables from -the first set. - -# Insert some data into one set of models. ->>> a = A01.objects.create(f_a="foo", f_b=42) ->>> _ = B01.objects.create(fk_a=a, f_a="fred", f_b=1729) ->>> c = C01.objects.create(f_a="barney", f_b=1) ->>> c.mm_a = [a] - -# ... and pull it out via the other set. ->>> A02.objects.all() -[] ->>> b = B02.objects.all()[0] ->>> b - ->>> b.fk_a - ->>> C02.objects.filter(f_a=None) -[] ->>> C02.objects.filter(mm_a=a.id) -[] - -"""} diff --git a/tests/modeltests/unmanaged_models/tests.py b/tests/modeltests/unmanaged_models/tests.py index c5f14bd3df05..dbbe848cce91 100644 --- a/tests/modeltests/unmanaged_models/tests.py +++ b/tests/modeltests/unmanaged_models/tests.py @@ -1,9 +1,46 @@ from django.test import TestCase from django.db import connection from models import Unmanaged1, Unmanaged2, Managed1 +from models import A01, A02, B01, B02, C01, C02 + +class SimpleTests(TestCase): + + def test_simple(self): + """ + The main test here is that the all the models can be created without + any database errors. We can also do some more simple insertion and + lookup tests whilst we're here to show that the second of models do + refer to the tables from the first set. + """ + # Insert some data into one set of models. + a = A01.objects.create(f_a="foo", f_b=42) + B01.objects.create(fk_a=a, f_a="fred", f_b=1729) + c = C01.objects.create(f_a="barney", f_b=1) + c.mm_a = [a] + + # ... and pull it out via the other set. + a2 = A02.objects.all()[0] + self.assertTrue(isinstance(a2, A02)) + self.assertEqual(a2.f_a, "foo") + + b2 = B02.objects.all()[0] + self.assertTrue(isinstance(b2, B02)) + self.assertEqual(b2.f_a, "fred") + + self.assertTrue(isinstance(b2.fk_a, A02)) + self.assertEqual(b2.fk_a.f_a, "foo") + + self.assertEqual(list(C02.objects.filter(f_a=None)), []) + + resp = list(C02.objects.filter(mm_a=a.id)) + self.assertEqual(len(resp), 1) + + self.assertTrue(isinstance(resp[0], C02)) + self.assertEqual(resp[0].f_a, 'barney') + class ManyToManyUnmanagedTests(TestCase): - + def test_many_to_many_between_unmanaged(self): """ The intermediary table between two unmanaged models should not be created. @@ -11,7 +48,7 @@ def test_many_to_many_between_unmanaged(self): table = Unmanaged2._meta.get_field('mm').m2m_db_table() tables = connection.introspection.table_names() self.assert_(table not in tables, "Table '%s' should not exist, but it does." % table) - + def test_many_to_many_between_unmanaged_and_managed(self): """ An intermediary table between a managed and an unmanaged model should be created. @@ -19,4 +56,3 @@ def test_many_to_many_between_unmanaged_and_managed(self): table = Managed1._meta.get_field('mm').m2m_db_table() tables = connection.introspection.table_names() self.assert_(table in tables, "Table '%s' does not exist." % table) - \ No newline at end of file diff --git a/tests/modeltests/update/models.py b/tests/modeltests/update/models.py index 9ce672f2b3fc..7b633e28dce8 100644 --- a/tests/modeltests/update/models.py +++ b/tests/modeltests/update/models.py @@ -33,59 +33,3 @@ class C(models.Model): class D(C): a = models.ForeignKey(A) - -__test__ = {'API_TESTS': """ ->>> DataPoint(name="d0", value="apple").save() ->>> DataPoint(name="d2", value="banana").save() ->>> d3 = DataPoint.objects.create(name="d3", value="banana") ->>> RelatedPoint(name="r1", data=d3).save() - -Objects are updated by first filtering the candidates into a queryset and then -calling the update() method. It executes immediately and returns nothing. - ->>> DataPoint.objects.filter(value="apple").update(name="d1") -1 ->>> DataPoint.objects.filter(value="apple") -[] - -We can update multiple objects at once. - ->>> DataPoint.objects.filter(value="banana").update(value="pineapple") -2 ->>> DataPoint.objects.get(name="d2").value -u'pineapple' - -Foreign key fields can also be updated, although you can only update the object -referred to, not anything inside the related object. - ->>> d = DataPoint.objects.get(name="d1") ->>> RelatedPoint.objects.filter(name="r1").update(data=d) -1 ->>> RelatedPoint.objects.filter(data__name="d1") -[] - -Multiple fields can be updated at once - ->>> DataPoint.objects.filter(value="pineapple").update(value="fruit", another_value="peaches") -2 ->>> d = DataPoint.objects.get(name="d2") ->>> d.value, d.another_value -(u'fruit', u'peaches') - -In the rare case you want to update every instance of a model, update() is also -a manager method. - ->>> DataPoint.objects.update(value='thing') -3 ->>> DataPoint.objects.values('value').distinct() -[{'value': u'thing'}] - -We do not support update on already sliced query sets. - ->>> DataPoint.objects.all()[:2].update(another_value='another thing') -Traceback (most recent call last): - ... -AssertionError: Cannot update a query once a slice has been taken. - -""" -} diff --git a/tests/modeltests/update/tests.py b/tests/modeltests/update/tests.py index 05397f8306a5..d0b6ea3ab0dc 100644 --- a/tests/modeltests/update/tests.py +++ b/tests/modeltests/update/tests.py @@ -1,6 +1,7 @@ from django.test import TestCase -from models import A, B, D +from models import A, B, C, D, DataPoint, RelatedPoint + class SimpleTest(TestCase): def setUp(self): @@ -15,18 +16,18 @@ def test_nonempty_update(self): Test that update changes the right number of rows for a nonempty queryset """ num_updated = self.a1.b_set.update(y=100) - self.failUnlessEqual(num_updated, 20) + self.assertEqual(num_updated, 20) cnt = B.objects.filter(y=100).count() - self.failUnlessEqual(cnt, 20) + self.assertEqual(cnt, 20) def test_empty_update(self): """ Test that update changes the right number of rows for an empty queryset """ num_updated = self.a2.b_set.update(y=100) - self.failUnlessEqual(num_updated, 0) + self.assertEqual(num_updated, 0) cnt = B.objects.filter(y=100).count() - self.failUnlessEqual(cnt, 0) + self.assertEqual(cnt, 0) def test_nonempty_update_with_inheritance(self): """ @@ -34,9 +35,9 @@ def test_nonempty_update_with_inheritance(self): when the update affects only a base table """ num_updated = self.a1.d_set.update(y=100) - self.failUnlessEqual(num_updated, 20) + self.assertEqual(num_updated, 20) cnt = D.objects.filter(y=100).count() - self.failUnlessEqual(cnt, 20) + self.assertEqual(cnt, 20) def test_empty_update_with_inheritance(self): """ @@ -44,6 +45,72 @@ def test_empty_update_with_inheritance(self): when the update affects only a base table """ num_updated = self.a2.d_set.update(y=100) - self.failUnlessEqual(num_updated, 0) + self.assertEqual(num_updated, 0) cnt = D.objects.filter(y=100).count() - self.failUnlessEqual(cnt, 0) + self.assertEqual(cnt, 0) + +class AdvancedTests(TestCase): + + def setUp(self): + self.d0 = DataPoint.objects.create(name="d0", value="apple") + self.d2 = DataPoint.objects.create(name="d2", value="banana") + self.d3 = DataPoint.objects.create(name="d3", value="banana") + self.r1 = RelatedPoint.objects.create(name="r1", data=self.d3) + + def test_update(self): + """ + Objects are updated by first filtering the candidates into a queryset + and then calling the update() method. It executes immediately and + returns nothing. + """ + resp = DataPoint.objects.filter(value="apple").update(name="d1") + self.assertEqual(resp, 1) + resp = DataPoint.objects.filter(value="apple") + self.assertEqual(list(resp), [self.d0]) + + def test_update_multiple_objects(self): + """ + We can update multiple objects at once. + """ + resp = DataPoint.objects.filter(value="banana").update( + value="pineapple") + self.assertEqual(resp, 2) + self.assertEqual(DataPoint.objects.get(name="d2").value, u'pineapple') + + def test_update_fk(self): + """ + Foreign key fields can also be updated, although you can only update + the object referred to, not anything inside the related object. + """ + resp = RelatedPoint.objects.filter(name="r1").update(data=self.d0) + self.assertEqual(resp, 1) + resp = RelatedPoint.objects.filter(data__name="d0") + self.assertEqual(list(resp), [self.r1]) + + def test_update_multiple_fields(self): + """ + Multiple fields can be updated at once + """ + resp = DataPoint.objects.filter(value="apple").update( + value="fruit", another_value="peach") + self.assertEqual(resp, 1) + d = DataPoint.objects.get(name="d0") + self.assertEqual(d.value, u'fruit') + self.assertEqual(d.another_value, u'peach') + + def test_update_all(self): + """ + In the rare case you want to update every instance of a model, update() + is also a manager method. + """ + self.assertEqual(DataPoint.objects.update(value='thing'), 3) + resp = DataPoint.objects.values('value').distinct() + self.assertEqual(list(resp), [{'value': u'thing'}]) + + def test_update_slice_fail(self): + """ + We do not support update on already sliced query sets. + """ + method = DataPoint.objects.all()[:2].update + self.assertRaises(AssertionError, method, + another_value='another thing') diff --git a/tests/modeltests/user_commands/management/commands/dance.py b/tests/modeltests/user_commands/management/commands/dance.py index a504d486d68a..acefe0927158 100644 --- a/tests/modeltests/user_commands/management/commands/dance.py +++ b/tests/modeltests/user_commands/management/commands/dance.py @@ -11,4 +11,4 @@ class Command(BaseCommand): ] def handle(self, *args, **options): - print "I don't feel like dancing %s." % options["style"] + self.stdout.write("I don't feel like dancing %s." % options["style"]) diff --git a/tests/modeltests/user_commands/models.py b/tests/modeltests/user_commands/models.py index 10ccdb8e2ce5..f2aa549bb972 100644 --- a/tests/modeltests/user_commands/models.py +++ b/tests/modeltests/user_commands/models.py @@ -12,22 +12,3 @@ ``django.core.management.commands`` directory. This directory contains the definitions for the base Django ``manage.py`` commands. """ - -__test__ = {'API_TESTS': """ ->>> from django.core import management - -# Invoke a simple user-defined command ->>> management.call_command('dance', style="Jive") -I don't feel like dancing Jive. - -# Invoke a command that doesn't exist ->>> management.call_command('explode') -Traceback (most recent call last): -... -CommandError: Unknown command: 'explode' - -# Invoke a command with default option `style` ->>> management.call_command('dance') -I don't feel like dancing Rock'n'Roll. - -"""} diff --git a/tests/modeltests/user_commands/tests.py b/tests/modeltests/user_commands/tests.py new file mode 100644 index 000000000000..84aa7a53d576 --- /dev/null +++ b/tests/modeltests/user_commands/tests.py @@ -0,0 +1,21 @@ +from StringIO import StringIO + +from django.test import TestCase +from django.core import management +from django.core.management.base import CommandError + +class CommandTests(TestCase): + def test_command(self): + out = StringIO() + management.call_command('dance', stdout=out) + self.assertEquals(out.getvalue(), + "I don't feel like dancing Rock'n'Roll.") + + def test_command_style(self): + out = StringIO() + management.call_command('dance', style='Jive', stdout=out) + self.assertEquals(out.getvalue(), + "I don't feel like dancing Jive.") + + def test_explode(self): + self.assertRaises(CommandError, management.call_command, ('explode',)) \ No newline at end of file diff --git a/tests/modeltests/validation/models.py b/tests/modeltests/validation/models.py index dd429368857f..861d1440fe78 100644 --- a/tests/modeltests/validation/models.py +++ b/tests/modeltests/validation/models.py @@ -63,3 +63,18 @@ class Article(models.Model): def clean(self): if self.pub_date is None: self.pub_date = datetime.now() + +class Post(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.DateField() + + def __unicode__(self): + return self.name + +class FlexibleDatePost(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.DateField(blank=True, null=True) diff --git a/tests/modeltests/validation/test_unique.py b/tests/modeltests/validation/test_unique.py index 1b966390c440..c1d1e5dbcf63 100644 --- a/tests/modeltests/validation/test_unique.py +++ b/tests/modeltests/validation/test_unique.py @@ -1,8 +1,9 @@ import unittest import datetime from django.conf import settings +from django.core.exceptions import ValidationError from django.db import connection -from models import CustomPKModel, UniqueTogetherModel, UniqueFieldsModel, UniqueForDateModel, ModelToValidate +from models import CustomPKModel, UniqueTogetherModel, UniqueFieldsModel, UniqueForDateModel, ModelToValidate, Post, FlexibleDatePost class GetUniqueCheckTests(unittest.TestCase): @@ -40,6 +41,15 @@ def test_unique_for_date_gets_picked_up(self): ), m._get_unique_checks() ) + def test_unique_for_date_exclusion(self): + m = UniqueForDateModel() + self.assertEqual(( + [(UniqueForDateModel, ('id',))], + [(UniqueForDateModel, 'year', 'count', 'end_date'), + (UniqueForDateModel, 'month', 'order', 'end_date')] + ), m._get_unique_checks(exclude='start_date') + ) + class PerformUniqueChecksTest(unittest.TestCase): def setUp(self): # Set debug to True to gain access to connection.queries. @@ -74,3 +84,74 @@ def test_primary_key_unique_check_not_performed_when_not_adding(self): mtv.full_clean() self.assertEqual(query_count, len(connection.queries)) + def test_unique_for_date(self): + p1 = Post.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + + p = Post(title="Django 1.0 is released", posted=datetime.date(2008, 9, 3)) + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'title': [u'Title must be unique for Posted date.']}) + else: + self.fail('unique_for_date checks should catch this.') + + # Should work without errors + p = Post(title="Work on Django 1.1 begins", posted=datetime.date(2008, 9, 3)) + p.full_clean() + + # Should work without errors + p = Post(title="Django 1.0 is released", posted=datetime.datetime(2008, 9,4)) + p.full_clean() + + p = Post(slug="Django 1.0", posted=datetime.datetime(2008, 1, 1)) + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'slug': [u'Slug must be unique for Posted year.']}) + else: + self.fail('unique_for_year checks should catch this.') + + p = Post(subtitle="Finally", posted=datetime.datetime(2008, 9, 30)) + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'subtitle': [u'Subtitle must be unique for Posted month.']}) + else: + self.fail('unique_for_month checks should catch this.') + + p = Post(title="Django 1.0 is released") + try: + p.full_clean() + except ValidationError, e: + self.assertEqual(e.message_dict, {'posted': [u'This field cannot be null.']}) + else: + self.fail("Model validation shouldn't allow an absent value for a DateField without null=True.") + + def test_unique_for_date_with_nullable_date(self): + p1 = FlexibleDatePost.objects.create(title="Django 1.0 is released", + slug="Django 1.0", subtitle="Finally", posted=datetime.date(2008, 9, 3)) + + p = FlexibleDatePost(title="Django 1.0 is released") + try: + p.full_clean() + except ValidationError, e: + self.fail("unique_for_date checks shouldn't trigger when the associated DateField is None.") + except: + self.fail("unique_for_date checks shouldn't explode when the associated DateField is None.") + + p = FlexibleDatePost(slug="Django 1.0") + try: + p.full_clean() + except ValidationError, e: + self.fail("unique_for_year checks shouldn't trigger when the associated DateField is None.") + except: + self.fail("unique_for_year checks shouldn't explode when the associated DateField is None.") + + p = FlexibleDatePost(subtitle="Finally") + try: + p.full_clean() + except ValidationError, e: + self.fail("unique_for_month checks shouldn't trigger when the associated DateField is None.") + except: + self.fail("unique_for_month checks shouldn't explode when the associated DateField is None.") diff --git a/tests/modeltests/validation/tests.py b/tests/modeltests/validation/tests.py index 00273931c7e3..6a6660e1004c 100644 --- a/tests/modeltests/validation/tests.py +++ b/tests/modeltests/validation/tests.py @@ -52,14 +52,6 @@ def test_wrong_url_value_raises_error(self): mtv = ModelToValidate(number=10, name='Some Name', url='not a url') self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'Enter a valid value.']) - def test_correct_url_but_nonexisting_gives_404(self): - mtv = ModelToValidate(number=10, name='Some Name', url='http://google.com/we-love-microsoft.html') - self.assertFieldFailsValidationWithMessage(mtv.full_clean, 'url', [u'This URL appears to be a broken link.']) - - def test_correct_url_value_passes(self): - mtv = ModelToValidate(number=10, name='Some Name', url='http://www.djangoproject.com/') - self.assertEqual(None, mtv.full_clean()) # This will fail if there's no Internet connection - def test_text_greater_that_charfields_max_length_eaises_erros(self): mtv = ModelToValidate(number=10, name='Some Name'*100) self.assertFailsValidation(mtv.full_clean, ['name',]) diff --git a/tests/regressiontests/admin_changelist/models.py b/tests/regressiontests/admin_changelist/models.py index f030a781e971..858d6dfd4587 100644 --- a/tests/regressiontests/admin_changelist/models.py +++ b/tests/regressiontests/admin_changelist/models.py @@ -5,5 +5,5 @@ class Parent(models.Model): name = models.CharField(max_length=128) class Child(models.Model): - parent = models.ForeignKey(Parent, editable=False) - name = models.CharField(max_length=30, blank=True) \ No newline at end of file + parent = models.ForeignKey(Parent, editable=False, null=True) + name = models.CharField(max_length=30, blank=True) diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py index b70d7c51f411..bb6e00a10cba 100644 --- a/tests/regressiontests/admin_changelist/tests.py +++ b/tests/regressiontests/admin_changelist/tests.py @@ -1,66 +1,161 @@ -import unittest from django.contrib import admin +from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.views.main import ChangeList from django.template import Context, Template +from django.test import TransactionTestCase from regressiontests.admin_changelist.models import Child, Parent -class ChangeListTests(unittest.TestCase): +class ChangeListTests(TransactionTestCase): def test_select_related_preserved(self): """ Regression test for #10348: ChangeList.get_query_set() shouldn't overwrite a custom select_related provided by ModelAdmin.queryset(). """ m = ChildAdmin(Child, admin.site) - cl = ChangeList(MockRequest(), Child, m.list_display, m.list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, + cl = ChangeList(MockRequest(), 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_editable, m) self.assertEqual(cl.query_set.query.select_related, {'parent': {'name': {}}}) + def test_result_list_empty_changelist_value(self): + """ + Regression test for #14982: EMPTY_CHANGELIST_VALUE should be honored + for relationship fields + """ + new_child = Child.objects.create(name='name', parent=None) + request = MockRequest() + m = ChildAdmin(Child, admin.site) + 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_editable, m) + cl.formset = None + template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') + context = Context({'cl': cl}) + table_output = template.render(context) + row_html = '
        ' % (new_child.id, new_child.id) + self.assertFalse(table_output.find(row_html) == -1, + 'Failed to find expected row element: %s' % table_output) + + def test_result_list_html(self): """ - Regression test for #11791: Inclusion tag result_list generates a - table and this checks that the items are nested within the table - element tags. + Verifies that inclusion tag result_list generates a table when with + default ModelAdmin settings. """ new_parent = Parent.objects.create(name='parent') new_child = Child.objects.create(name='name', parent=new_parent) request = MockRequest() m = ChildAdmin(Child, admin.site) - cl = ChangeList(request, Child, m.list_display, m.list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, + 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_editable, m) - FormSet = m.get_changelist_formset(request) - cl.formset = FormSet(queryset=cl.result_list) + cl.formset = None template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') context = Context({'cl': cl}) table_output = template.render(context) - hidden_input_elem = '' - self.failIf(table_output.find(hidden_input_elem) == -1, - 'Failed to find expected hidden input element in: %s' % table_output) - self.failIf(table_output.find('' % hidden_input_elem) == -1, - 'Hidden input element is not enclosed in ' % (new_child.id, new_child.id) + self.assertFalse(table_output.find(row_html) == -1, + 'Failed to find expected row element: %s' % table_output) + + def test_result_list_editable_html(self): + """ + Regression tests for #11791: Inclusion tag result_list generates a + table and this checks that the items are nested within the table + element tags. + Also a regression test for #13599, verifies that hidden fields + when list_editable is enabled are rendered in a div outside the + table. + """ + new_parent = Parent.objects.create(name='parent') + new_child = Child.objects.create(name='name', parent=new_parent) + request = MockRequest() + m = ChildAdmin(Child, admin.site) # Test with list_editable fields m.list_display = ['id', 'name', 'parent'] m.list_display_links = ['id'] m.list_editable = ['name'] - cl = ChangeList(request, Child, m.list_display, m.list_display_links, - m.list_filter, m.date_hierarchy, m.search_fields, + 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_editable, m) FormSet = m.get_changelist_formset(request) cl.formset = FormSet(queryset=cl.result_list) template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') context = Context({'cl': cl}) table_output = template.render(context) - self.failIf(table_output.find(hidden_input_elem) == -1, - 'Failed to find expected hidden input element in: %s' % table_output) - self.failIf(table_output.find('' % hidden_input_elem) == -1, - 'Hidden input element is not enclosed in ' % editable_name_field == -1, + 'Failed to find "name" list_editable field in: %s' % table_output) + + def test_result_list_editable(self): + """ + Regression test for #14312: list_editable with pagination + """ + + new_parent = Parent.objects.create(name='parent') + for i in range(200): + new_child = Child.objects.create(name='name %s' % i, parent=new_parent) + request = MockRequest() + request.GET['p'] = -1 # Anything outside range + m = ChildAdmin(Child, admin.site) + + # Test with list_editable fields + m.list_display = ['id', 'name', 'parent'] + m.list_display_links = ['id'] + m.list_editable = ['name'] + self.assertRaises(IncorrectLookupParameters, lambda: \ + 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_editable, m)) + + def test_pagination(self): + """ + Regression tests for #12893: Pagination in admins changelist doesn't + use queryset set by modeladmin. + """ + parent = Parent.objects.create(name='anything') + for i in range(30): + Child.objects.create(name='name %s' % i, parent=parent) + Child.objects.create(name='filtered %s' % i, parent=parent) + + request = MockRequest() + + # Test default queryset + m = ChildAdmin(Child, admin.site) + 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_editable, m) + self.assertEqual(cl.query_set.count(), 60) + self.assertEqual(cl.paginator.count, 60) + self.assertEqual(cl.paginator.page_range, [1, 2, 3, 4, 5, 6]) + + # Test custom queryset + m = FilteredChildAdmin(Child, admin.site) + 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_editable, m) + self.assertEqual(cl.query_set.count(), 30) + self.assertEqual(cl.paginator.count, 30) + self.assertEqual(cl.paginator.page_range, [1, 2, 3]) + class ChildAdmin(admin.ModelAdmin): list_display = ['name', 'parent'] + list_per_page = 10 def queryset(self, request): return super(ChildAdmin, self).queryset(request).select_related("parent__name") +class FilteredChildAdmin(admin.ModelAdmin): + list_display = ['name', 'parent'] + list_per_page = 10 + def queryset(self, request): + return super(FilteredChildAdmin, self).queryset(request).filter( + name__contains='filtered') + class MockRequest(object): GET = {} diff --git a/tests/regressiontests/admin_inlines/models.py b/tests/regressiontests/admin_inlines/models.py index 5a12e0743c13..ee0abd1d3d4e 100644 --- a/tests/regressiontests/admin_inlines/models.py +++ b/tests/regressiontests/admin_inlines/models.py @@ -6,6 +6,7 @@ from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic +from django import forms class Parent(models.Model): name = models.CharField(max_length=50) @@ -124,24 +125,69 @@ class InlineWeakness(admin.TabularInline): admin.site.register(Fashionista, inlines=[InlineWeakness]) +# Models for #13510 -__test__ = {'API_TESTS': """ +class TitleCollection(models.Model): + pass -# Regression test for #9362 +class Title(models.Model): + collection = models.ForeignKey(TitleCollection, blank=True, null=True) + title1 = models.CharField(max_length=100) + title2 = models.CharField(max_length=100) ->>> sally = Teacher.objects.create(name='Sally') ->>> john = Parent.objects.create(name='John') ->>> joe = Child.objects.create(name='Joe', teacher=sally, parent=john) +class TitleForm(forms.ModelForm): -The problem depends only on InlineAdminForm and its "original" argument, so -we can safely set the other arguments to None/{}. We just need to check that -the content_type argument of Child isn't altered by the internals of the -inline form. + def clean(self): + cleaned_data = self.cleaned_data + title1 = cleaned_data.get("title1") + title2 = cleaned_data.get("title2") + if title1 != title2: + raise forms.ValidationError("The two titles must be the same") + return cleaned_data ->>> from django.contrib.admin.helpers import InlineAdminForm ->>> iaf = InlineAdminForm(None, None, {}, {}, joe) ->>> iaf.original - +class TitleInline(admin.TabularInline): + model = Title + form = TitleForm + extra = 1 -""" -} +admin.site.register(TitleCollection, inlines=[TitleInline]) + +# Models for #15424 + +class Poll(models.Model): + name = models.CharField(max_length=40) + +class Question(models.Model): + poll = models.ForeignKey(Poll) + +class QuestionInline(admin.TabularInline): + model = Question + readonly_fields=['call_me'] + + def call_me(self, obj): + return 'Callable in QuestionInline' + +class PollAdmin(admin.ModelAdmin): + inlines = [QuestionInline] + + def call_me(self, obj): + return 'Callable in PollAdmin' + +class Novel(models.Model): + name = models.CharField(max_length=40) + +class Chapter(models.Model): + novel = models.ForeignKey(Novel) + +class ChapterInline(admin.TabularInline): + model = Chapter + readonly_fields=['call_me'] + + def call_me(self, obj): + return 'Callable in ChapterInline' + +class NovelAdmin(admin.ModelAdmin): + inlines = [ChapterInline] + +admin.site.register(Poll, PollAdmin) +admin.site.register(Novel, NovelAdmin) diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py index c27873ceecbc..067b3c5eaf26 100644 --- a/tests/regressiontests/admin_inlines/tests.py +++ b/tests/regressiontests/admin_inlines/tests.py @@ -1,9 +1,11 @@ +from django.contrib.admin.helpers import InlineAdminForm +from django.contrib.contenttypes.models import ContentType from django.test import TestCase # local test models -from models import Holder, Inner, InnerInline -from models import Holder2, Inner2, Holder3, Inner3 -from models import Person, OutfitItem, Fashionista +from models import (Holder, Inner, InnerInline, Holder2, Inner2, Holder3, + Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child) + class TestInline(TestCase): fixtures = ['admin-views-users.xml'] @@ -15,7 +17,7 @@ def setUp(self): self.change_url = '/test_admin/admin/admin_inlines/holder/%i/' % holder.id result = self.client.login(username='super', password='secret') - self.failUnlessEqual(result, True) + self.assertEqual(result, True) def tearDown(self): self.client.logout() @@ -65,13 +67,49 @@ def test_inline_primary(self): self.assertEqual(response.status_code, 302) self.assertEqual(len(Fashionista.objects.filter(person__firstname='Imelda')), 1) + def test_tabular_non_field_errors(self): + """ + Ensure that non_field_errors are displayed correctly, including the + right value for colspan. Refs #13510. + """ + data = { + 'title_set-TOTAL_FORMS': 1, + 'title_set-INITIAL_FORMS': 0, + 'title_set-MAX_NUM_FORMS': 0, + '_save': u'Save', + 'title_set-0-title1': 'a title', + 'title_set-0-title2': 'a different title', + } + response = self.client.post('/test_admin/admin/admin_inlines/titlecollection/add/', data) + # Here colspan is "4": two fields (title1 and title2), one hidden field and the delete checkbock. + self.assertContains(response, '') + + def test_no_parent_callable_lookup(self): + """Admin inline `readonly_field` shouldn't invoke parent ModelAdmin callable""" + # Identically named callable isn't present in the parent ModelAdmin, + # rendering of the add view shouldn't explode + response = self.client.get('/test_admin/admin/admin_inlines/novel/add/') + self.assertEqual(response.status_code, 200) + # View should have the child inlines section + self.assertContains(response, '
        ') + + def test_callable_lookup(self): + """Admin inline should invoke local callable when its name is listed in readonly_fields""" + response = self.client.get('/test_admin/admin/admin_inlines/poll/add/') + self.assertEqual(response.status_code, 200) + # Add parent object view should have the child inlines section + self.assertContains(response, '
        ') + # The right callabe should be used for the inline readonly_fields + # column cells + self.assertContains(response, '

        Callable in QuestionInline

        ') + class TestInlineMedia(TestCase): fixtures = ['admin-views-users.xml'] def setUp(self): result = self.client.login(username='super', password='secret') - self.failUnlessEqual(result, True) + self.assertEqual(result, True) def tearDown(self): self.client.logout() @@ -100,3 +138,20 @@ def test_all_inline_media(self): response = self.client.get(change_url) self.assertContains(response, 'my_awesome_admin_scripts.js') self.assertContains(response, 'my_awesome_inline_scripts.js') + +class TestInlineAdminForm(TestCase): + + def test_immutable_content_type(self): + """Regression for #9362 + The problem depends only on InlineAdminForm and its "original" + argument, so we can safely set the other arguments to None/{}. We just + need to check that the content_type argument of Child isn't altered by + the internals of the inline form.""" + + sally = Teacher.objects.create(name='Sally') + john = Parent.objects.create(name='John') + joe = Child.objects.create(name='Joe', teacher=sally, parent=john) + + iaf = InlineAdminForm(None, None, {}, {}, joe) + parent_ct = ContentType.objects.get_for_model(Parent) + self.assertEqual(iaf.original.content_type, parent_ct) diff --git a/tests/regressiontests/admin_ordering/models.py b/tests/regressiontests/admin_ordering/models.py index 601f06bb0aeb..ad63685b2f3a 100644 --- a/tests/regressiontests/admin_ordering/models.py +++ b/tests/regressiontests/admin_ordering/models.py @@ -8,39 +8,3 @@ class Band(models.Model): class Meta: ordering = ('name',) - -__test__ = {'API_TESTS': """ - -Let's make sure that ModelAdmin.queryset uses the ordering we define in -ModelAdmin rather that ordering defined in the model's inner Meta -class. - ->>> from django.contrib.admin.options import ModelAdmin - ->>> b1 = Band(name='Aerosmith', bio='', rank=3) ->>> b1.save() ->>> b2 = Band(name='Radiohead', bio='', rank=1) ->>> b2.save() ->>> b3 = Band(name='Van Halen', bio='', rank=2) ->>> b3.save() - -The default ordering should be by name, as specified in the inner Meta class. - ->>> ma = ModelAdmin(Band, None) ->>> [b.name for b in ma.queryset(None)] -[u'Aerosmith', u'Radiohead', u'Van Halen'] - - -Let's use a custom ModelAdmin that changes the ordering, and make sure it -actually changes. - ->>> class BandAdmin(ModelAdmin): -... ordering = ('rank',) # default ordering is ('name',) -... - ->>> ma = BandAdmin(Band, None) ->>> [b.name for b in ma.queryset(None)] -[u'Radiohead', u'Van Halen', u'Aerosmith'] - -""" -} diff --git a/tests/regressiontests/admin_ordering/tests.py b/tests/regressiontests/admin_ordering/tests.py new file mode 100644 index 000000000000..f63f202ce165 --- /dev/null +++ b/tests/regressiontests/admin_ordering/tests.py @@ -0,0 +1,39 @@ +from django.test import TestCase +from django.contrib.admin.options import ModelAdmin + +from models import Band + +class TestAdminOrdering(TestCase): + """ + Let's make sure that ModelAdmin.queryset uses the ordering we define in + ModelAdmin rather that ordering defined in the model's inner Meta + class. + """ + + def setUp(self): + b1 = Band(name='Aerosmith', bio='', rank=3) + b1.save() + b2 = Band(name='Radiohead', bio='', rank=1) + b2.save() + b3 = Band(name='Van Halen', bio='', rank=2) + b3.save() + + def test_default_ordering(self): + """ + The default ordering should be by name, as specified in the inner Meta + class. + """ + ma = ModelAdmin(Band, None) + names = [b.name for b in ma.queryset(None)] + self.assertEqual([u'Aerosmith', u'Radiohead', u'Van Halen'], names) + + def test_specified_ordering(self): + """ + Let's use a custom ModelAdmin that changes the ordering, and make sure + it actually changes. + """ + class BandAdmin(ModelAdmin): + ordering = ('rank',) # default ordering is ('name',) + ma = BandAdmin(Band, None) + names = [b.name for b in ma.queryset(None)] + self.assertEqual([u'Radiohead', u'Van Halen', u'Aerosmith'], names) diff --git a/tests/regressiontests/admin_registration/models.py b/tests/regressiontests/admin_registration/models.py index 35cf8afce8a8..4a2d4e9614b2 100644 --- a/tests/regressiontests/admin_registration/models.py +++ b/tests/regressiontests/admin_registration/models.py @@ -3,62 +3,9 @@ """ from django.db import models -from django.contrib import admin class Person(models.Model): name = models.CharField(max_length=200) class Place(models.Model): name = models.CharField(max_length=200) - -__test__ = {'API_TESTS':""" - - -# Bare registration ->>> site = admin.AdminSite() ->>> site.register(Person) ->>> site._registry[Person] - - -# Registration with a ModelAdmin ->>> site = admin.AdminSite() ->>> class NameAdmin(admin.ModelAdmin): -... list_display = ['name'] -... save_on_top = True - ->>> site.register(Person, NameAdmin) ->>> site._registry[Person] - - -# You can't register the same model twice ->>> site.register(Person) -Traceback (most recent call last): - ... -AlreadyRegistered: The model Person is already registered - -# Registration using **options ->>> site = admin.AdminSite() ->>> site.register(Person, search_fields=['name']) ->>> site._registry[Person].search_fields -['name'] - -# With both admin_class and **options the **options override the fields in -# the admin class. ->>> site = admin.AdminSite() ->>> site.register(Person, NameAdmin, search_fields=["name"], list_display=['__str__']) ->>> site._registry[Person].search_fields -['name'] ->>> site._registry[Person].list_display -['action_checkbox', '__str__'] ->>> site._registry[Person].save_on_top -True - -# You can also register iterables instead of single classes -- a nice shortcut ->>> site = admin.AdminSite() ->>> site.register([Person, Place], search_fields=['name']) ->>> site._registry[Person] - ->>> site._registry[Place] - - -"""} diff --git a/tests/regressiontests/admin_registration/tests.py b/tests/regressiontests/admin_registration/tests.py new file mode 100644 index 000000000000..e2a5d7e01791 --- /dev/null +++ b/tests/regressiontests/admin_registration/tests.py @@ -0,0 +1,54 @@ +from django.test import TestCase + +from django.contrib import admin + +from models import Person, Place + +class NameAdmin(admin.ModelAdmin): + list_display = ['name'] + save_on_top = True + +class TestRegistration(TestCase): + def setUp(self): + self.site = admin.AdminSite() + + def test_bare_registration(self): + self.site.register(Person) + self.assertTrue( + isinstance(self.site._registry[Person], admin.options.ModelAdmin) + ) + + def test_registration_with_model_admin(self): + self.site.register(Person, NameAdmin) + self.assertTrue( + isinstance(self.site._registry[Person], NameAdmin) + ) + + def test_prevent_double_registration(self): + self.site.register(Person) + self.assertRaises(admin.sites.AlreadyRegistered, + self.site.register, + Person) + + def test_registration_with_star_star_options(self): + self.site.register(Person, search_fields=['name']) + self.assertEqual(self.site._registry[Person].search_fields, ['name']) + + def test_star_star_overrides(self): + self.site.register(Person, NameAdmin, + search_fields=["name"], list_display=['__str__']) + self.assertEqual(self.site._registry[Person].search_fields, ['name']) + self.assertEqual(self.site._registry[Person].list_display, + ['action_checkbox', '__str__']) + self.assertTrue(self.site._registry[Person].save_on_top) + + def test_iterable_registration(self): + self.site.register([Person, Place], search_fields=['name']) + self.assertTrue( + isinstance(self.site._registry[Person], admin.options.ModelAdmin) + ) + self.assertEqual(self.site._registry[Person].search_fields, ['name']) + self.assertTrue( + isinstance(self.site._registry[Place], admin.options.ModelAdmin) + ) + self.assertEqual(self.site._registry[Place].search_fields, ['name']) diff --git a/tests/regressiontests/admin_scripts/tests.py b/tests/regressiontests/admin_scripts/tests.py index 7ec245456122..b3e826f74b32 100644 --- a/tests/regressiontests/admin_scripts/tests.py +++ b/tests/regressiontests/admin_scripts/tests.py @@ -12,6 +12,7 @@ from django import conf, bin, get_version from django.conf import settings + class AdminScriptTestCase(unittest.TestCase): def write_settings(self, filename, apps=None, is_dir=False, sdict=None): test_dir = os.path.dirname(os.path.dirname(__file__)) @@ -118,6 +119,7 @@ def run_test(self, script, args, settings_file=None, apps=None): from subprocess import Popen, PIPE p = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) stdin, stdout, stderr = (p.stdin, p.stdout, p.stderr) + p.wait() except ImportError: stdin, stdout, stderr = os.popen3(cmd) out, err = stdout.read(), stderr.read() @@ -133,7 +135,7 @@ def run_test(self, script, args, settings_file=None, apps=None): return out, err def run_django_admin(self, args, settings_file=None): - bin_dir = os.path.dirname(bin.__file__) + bin_dir = os.path.abspath(os.path.dirname(bin.__file__)) return self.run_test(os.path.join(bin_dir,'django-admin.py'), args, settings_file) def run_manage(self, args, settings_file=None): @@ -156,7 +158,7 @@ def assertNoOutput(self, stream): self.assertEquals(len(stream), 0, "Stream should be empty: actually contains '%s'" % stream) def assertOutput(self, stream, msg): "Utility assertion: assert that the given message exists in the output" - self.failUnless(msg in stream, "'%s' does not match actual output text '%s'" % (msg, stream)) + self.assertTrue(msg in stream, "'%s' does not match actual output text '%s'" % (msg, stream)) ########################################################################## # DJANGO ADMIN TESTS @@ -1029,6 +1031,16 @@ def test_help(self): self.assertOutput(out, "Usage: manage.py subcommand [options] [args]") self.assertOutput(err, "Type 'manage.py help ' for help on a specific subcommand.") + def test_short_help(self): + "-h is handled as a short form of --help" + args = ['-h'] + out, err = self.run_manage(args) + if sys.version_info < (2, 5): + self.assertOutput(out, "usage: manage.py subcommand [options] [args]") + else: + self.assertOutput(out, "Usage: manage.py subcommand [options] [args]") + self.assertOutput(err, "Type 'manage.py help ' for help on a specific subcommand.") + def test_specific_help(self): "--help can be used on a specific command" args = ['sqlall','--help'] diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py index 493e1271adee..3191a55a2b14 100644 --- a/tests/regressiontests/admin_util/models.py +++ b/tests/regressiontests/admin_util/models.py @@ -1,7 +1,5 @@ from django.db import models - - class Article(models.Model): """ A simple Article model for testing @@ -20,3 +18,16 @@ def test_from_model_with_override(self): class Count(models.Model): num = models.PositiveSmallIntegerField() + +class Event(models.Model): + date = models.DateTimeField(auto_now_add=True) + +class Location(models.Model): + event = models.OneToOneField(Event, verbose_name='awesome event') + +class Guest(models.Model): + event = models.OneToOneField(Event) + name = models.CharField(max_length=255) + + class Meta: + verbose_name = "awesome guest" diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py index 5ea0ac585ef6..7476d10f28ba 100644 --- a/tests/regressiontests/admin_util/tests.py +++ b/tests/regressiontests/admin_util/tests.py @@ -12,7 +12,7 @@ from django.contrib.sites.models import Site from django.contrib.admin.util import NestedObjects -from models import Article, Count +from models import Article, Count, Event, Location class NestedObjectsTests(TestCase): @@ -220,3 +220,20 @@ def test_from_model(self, obj): ), ("not Really the Model", MockModelAdmin.test_from_model) ) + + def test_related_name(self): + """ + Regression test for #13963 + """ + self.assertEquals( + label_for_field('location', Event, return_attr=True), + ('location', None), + ) + self.assertEquals( + label_for_field('event', Location, return_attr=True), + ('awesome event', None), + ) + self.assertEquals( + label_for_field('guest', Event, return_attr=True), + ('awesome guest', None), + ) diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py index d9275745388d..5e080a92325d 100644 --- a/tests/regressiontests/admin_validation/models.py +++ b/tests/regressiontests/admin_validation/models.py @@ -47,218 +47,9 @@ class AuthorsBooks(models.Model): book = models.ForeignKey(Book) -__test__ = {'API_TESTS':""" +class State(models.Model): + name = models.CharField(max_length=15) ->>> from django import forms ->>> from django.contrib import admin ->>> from django.contrib.admin.validation import validate, validate_inline -# Regression test for #8027: custom ModelForms with fields/fieldsets - ->>> class SongForm(forms.ModelForm): -... pass - ->>> class ValidFields(admin.ModelAdmin): -... form = SongForm -... fields = ['title'] - ->>> class InvalidFields(admin.ModelAdmin): -... form = SongForm -... fields = ['spam'] - ->>> validate(ValidFields, Song) ->>> validate(InvalidFields, Song) -Traceback (most recent call last): - ... -ImproperlyConfigured: 'InvalidFields.fields' refers to field 'spam' that is missing from the form. - -# Tests for basic validation of 'exclude' option values (#12689) - ->>> class ExcludedFields1(admin.ModelAdmin): -... exclude = ('foo') - ->>> validate(ExcludedFields1, Book) -Traceback (most recent call last): - ... -ImproperlyConfigured: 'ExcludedFields1.exclude' must be a list or tuple. - ->>> class ExcludedFields2(admin.ModelAdmin): -... exclude = ('name', 'name') - ->>> validate(ExcludedFields2, Book) -Traceback (most recent call last): - ... -ImproperlyConfigured: There are duplicate field(s) in ExcludedFields2.exclude - ->>> class ExcludedFieldsInline(admin.TabularInline): -... model = Song -... exclude = ('foo') - ->>> class ExcludedFieldsAlbumAdmin(admin.ModelAdmin): -... model = Album -... inlines = [ExcludedFieldsInline] - ->>> validate(ExcludedFieldsAlbumAdmin, Album) -Traceback (most recent call last): - ... -ImproperlyConfigured: 'ExcludedFieldsInline.exclude' must be a list or tuple. - -# Regression test for #9932 - exclude in InlineModelAdmin -# should not contain the ForeignKey field used in ModelAdmin.model - ->>> class SongInline(admin.StackedInline): -... model = Song -... exclude = ['album'] - ->>> class AlbumAdmin(admin.ModelAdmin): -... model = Album -... inlines = [SongInline] - ->>> validate(AlbumAdmin, Album) -Traceback (most recent call last): - ... -ImproperlyConfigured: SongInline cannot exclude the field 'album' - this is the foreign key to the parent model Album. - -# Regression test for #11709 - when testing for fk excluding (when exclude is -# given) make sure fk_name is honored or things blow up when there is more -# than one fk to the parent model. - ->>> class TwoAlbumFKAndAnEInline(admin.TabularInline): -... model = TwoAlbumFKAndAnE -... exclude = ("e",) -... fk_name = "album1" - ->>> validate_inline(TwoAlbumFKAndAnEInline, None, Album) - -# Ensure inlines validate that they can be used correctly. - ->>> class TwoAlbumFKAndAnEInline(admin.TabularInline): -... model = TwoAlbumFKAndAnE - ->>> validate_inline(TwoAlbumFKAndAnEInline, None, Album) -Traceback (most recent call last): - ... -Exception: has more than 1 ForeignKey to - ->>> class TwoAlbumFKAndAnEInline(admin.TabularInline): -... model = TwoAlbumFKAndAnE -... fk_name = "album1" - ->>> validate_inline(TwoAlbumFKAndAnEInline, None, Album) - ->>> class SongAdmin(admin.ModelAdmin): -... readonly_fields = ("title",) - ->>> validate(SongAdmin, Song) - ->>> def my_function(obj): -... # does nothing -... pass ->>> class SongAdmin(admin.ModelAdmin): -... readonly_fields = (my_function,) - ->>> validate(SongAdmin, Song) - ->>> class SongAdmin(admin.ModelAdmin): -... readonly_fields = ("readonly_method_on_modeladmin",) -... -... def readonly_method_on_modeladmin(self, obj): -... # does nothing -... pass - ->>> validate(SongAdmin, Song) - ->>> class SongAdmin(admin.ModelAdmin): -... readonly_fields = ("readonly_method_on_model",) - ->>> validate(SongAdmin, Song) - ->>> class SongAdmin(admin.ModelAdmin): -... readonly_fields = ("title", "nonexistant") - ->>> validate(SongAdmin, Song) -Traceback (most recent call last): - ... -ImproperlyConfigured: SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'. - ->>> class SongAdmin(admin.ModelAdmin): -... readonly_fields = ("title", "awesome_song") -... fields = ("album", "title", "awesome_song") - ->>> validate(SongAdmin, Song) -Traceback (most recent call last): - ... -ImproperlyConfigured: SongAdmin.readonly_fields[1], 'awesome_song' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'. - ->>> class SongAdmin(SongAdmin): -... def awesome_song(self, instance): -... if instance.title == "Born to Run": -... return "Best Ever!" -... return "Status unknown." - ->>> validate(SongAdmin, Song) - ->>> class SongAdmin(admin.ModelAdmin): -... readonly_fields = (lambda obj: "test",) - ->>> validate(SongAdmin, Song) - -# Regression test for #12203/#12237 - Fail more gracefully when a M2M field that -# specifies the 'through' option is included in the 'fields' or the 'fieldsets' -# ModelAdmin options. - ->>> class BookAdmin(admin.ModelAdmin): -... fields = ['authors'] - ->>> validate(BookAdmin, Book) -Traceback (most recent call last): - ... -ImproperlyConfigured: 'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model. - ->>> class FieldsetBookAdmin(admin.ModelAdmin): -... fieldsets = ( -... ('Header 1', {'fields': ('name',)}), -... ('Header 2', {'fields': ('authors',)}), -... ) - ->>> validate(FieldsetBookAdmin, Book) -Traceback (most recent call last): - ... -ImproperlyConfigured: 'FieldsetBookAdmin.fieldsets[1][1]['fields']' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model. - ->>> class NestedFieldsetAdmin(admin.ModelAdmin): -... fieldsets = ( -... ('Main', {'fields': ('price', ('name', 'subtitle'))}), -... ) - ->>> validate(NestedFieldsetAdmin, Book) - -# Regression test for #12209 -- If the explicitly provided through model -# is specified as a string, the admin should still be able use -# Model.m2m_field.through - ->>> class AuthorsInline(admin.TabularInline): -... model = Book.authors.through - ->>> class BookAdmin(admin.ModelAdmin): -... inlines = [AuthorsInline] - -# If the through model is still a string (and hasn't been resolved to a model) -# the validation will fail. ->>> validate(BookAdmin, Book) - -# Regression for ensuring ModelAdmin.fields can contain non-model fields -# that broke with r11737 - ->>> class SongForm(forms.ModelForm): -... extra_data = forms.CharField() -... class Meta: -... model = Song - ->>> class FieldsOnFormOnlyAdmin(admin.ModelAdmin): -... form = SongForm -... fields = ['title', 'extra_data'] - ->>> validate(FieldsOnFormOnlyAdmin, Song) - -"""} +class City(models.Model): + state = models.ForeignKey(State) diff --git a/tests/regressiontests/admin_validation/tests.py b/tests/regressiontests/admin_validation/tests.py index 9166360ae3d2..6fbdc8040e4f 100644 --- a/tests/regressiontests/admin_validation/tests.py +++ b/tests/regressiontests/admin_validation/tests.py @@ -1,11 +1,30 @@ from django.contrib import admin -from django.contrib.admin.validation import validate +from django import forms +from django.contrib.admin.validation import validate, validate_inline, \ + ImproperlyConfigured from django.test import TestCase -from models import Song +from models import Song, Book, Album, TwoAlbumFKAndAnE, State, City +class SongForm(forms.ModelForm): + pass + +class ValidFields(admin.ModelAdmin): + form = SongForm + fields = ['title'] + +class InvalidFields(admin.ModelAdmin): + form = SongForm + fields = ['spam'] class ValidationTestCase(TestCase): + def assertRaisesMessage(self, exc, msg, func, *args, **kwargs): + try: + func(*args, **kwargs) + except Exception, e: + self.assertEqual(msg, str(e)) + self.assertTrue(isinstance(e, exc), "Expected %s, got %s" % (exc, type(e))) + def test_readonly_and_editable(self): class SongAdmin(admin.ModelAdmin): readonly_fields = ["original_release"] @@ -14,5 +33,221 @@ class SongAdmin(admin.ModelAdmin): "fields": ["title", "original_release"], }), ] - validate(SongAdmin, Song) + + def test_custom_modelforms_with_fields_fieldsets(self): + """ + # Regression test for #8027: custom ModelForms with fields/fieldsets + """ + validate(ValidFields, Song) + self.assertRaisesMessage(ImproperlyConfigured, + "'InvalidFields.fields' refers to field 'spam' that is missing from the form.", + validate, + InvalidFields, Song) + + def test_exclude_values(self): + """ + Tests for basic validation of 'exclude' option values (#12689) + """ + class ExcludedFields1(admin.ModelAdmin): + exclude = ('foo') + self.assertRaisesMessage(ImproperlyConfigured, + "'ExcludedFields1.exclude' must be a list or tuple.", + validate, + ExcludedFields1, 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) + + def test_exclude_in_inline(self): + class ExcludedFieldsInline(admin.TabularInline): + model = Song + exclude = ('foo') + + class ExcludedFieldsAlbumAdmin(admin.ModelAdmin): + model = Album + inlines = [ExcludedFieldsInline] + + self.assertRaisesMessage(ImproperlyConfigured, + "'ExcludedFieldsInline.exclude' must be a list or tuple.", + validate, + ExcludedFieldsAlbumAdmin, Album) + + def test_exclude_inline_model_admin(self): + """ + # Regression test for #9932 - exclude in InlineModelAdmin + # should not contain the ForeignKey field used in ModelAdmin.model + """ + class SongInline(admin.StackedInline): + model = Song + exclude = ['album'] + + class AlbumAdmin(admin.ModelAdmin): + model = Album + inlines = [SongInline] + + self.assertRaisesMessage(ImproperlyConfigured, + "SongInline cannot exclude the field 'album' - this is the foreign key to the parent model Album.", + validate, + AlbumAdmin, Album) + + def test_fk_exclusion(self): + """ + Regression test for #11709 - when testing for fk excluding (when exclude is + given) make sure fk_name is honored or things blow up when there is more + than one fk to the parent model. + """ + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + exclude = ("e",) + fk_name = "album1" + validate_inline(TwoAlbumFKAndAnEInline, None, Album) + + def test_inline_self_validation(self): + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + + self.assertRaisesMessage(Exception, + " has more than 1 ForeignKey to ", + validate_inline, + TwoAlbumFKAndAnEInline, None, Album) + + def test_inline_with_specified(self): + class TwoAlbumFKAndAnEInline(admin.TabularInline): + model = TwoAlbumFKAndAnE + fk_name = "album1" + validate_inline(TwoAlbumFKAndAnEInline, None, Album) + + def test_readonly(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("title",) + + validate(SongAdmin, Song) + + def test_readonly_on_method(self): + def my_function(obj): + pass + + class SongAdmin(admin.ModelAdmin): + readonly_fields = (my_function,) + + validate(SongAdmin, Song) + + def test_readonly_on_modeladmin(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("readonly_method_on_modeladmin",) + + def readonly_method_on_modeladmin(self, obj): + pass + + validate(SongAdmin, Song) + + def test_readonly_method_on_model(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("readonly_method_on_model",) + + validate(SongAdmin, Song) + + def test_nonexistant_field(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = ("title", "nonexistant") + + 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) + + def test_nonexistant_field_on_inline(self): + class CityInline(admin.TabularInline): + model = City + readonly_fields=['i_dont_exist'] # Missing attribute + + 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) + + def test_extra(self): + class SongAdmin(admin.ModelAdmin): + def awesome_song(self, instance): + if instance.title == "Born to Run": + return "Best Ever!" + return "Status unknown." + validate(SongAdmin, Song) + + def test_readonly_lambda(self): + class SongAdmin(admin.ModelAdmin): + readonly_fields = (lambda obj: "test",) + + validate(SongAdmin, Song) + + def test_graceful_m2m_fail(self): + """ + Regression test for #12203/#12237 - Fail more gracefully when a M2M field that + specifies the 'through' option is included in the 'fields' or the 'fieldsets' + ModelAdmin options. + """ + + class BookAdmin(admin.ModelAdmin): + fields = ['authors'] + + self.assertRaisesMessage(ImproperlyConfigured, + "'BookAdmin.fields' can't include the ManyToManyField field 'authors' because 'authors' manually specifies a 'through' model.", + validate, + BookAdmin, Book) + + def test_cannon_include_through(self): + class FieldsetBookAdmin(admin.ModelAdmin): + fieldsets = ( + ('Header 1', {'fields': ('name',)}), + ('Header 2', {'fields': ('authors',)}), + ) + 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) + + def test_nested_fieldsets(self): + class NestedFieldsetAdmin(admin.ModelAdmin): + fieldsets = ( + ('Main', {'fields': ('price', ('name', 'subtitle'))}), + ) + validate(NestedFieldsetAdmin, Book) + + def test_explicit_through_override(self): + """ + Regression test for #12209 -- If the explicitly provided through model + is specified as a string, the admin should still be able use + Model.m2m_field.through + """ + + class AuthorsInline(admin.TabularInline): + model = Book.authors.through + + class BookAdmin(admin.ModelAdmin): + inlines = [AuthorsInline] + + # If the through model is still a string (and hasn't been resolved to a model) + # the validation will fail. + validate(BookAdmin, Book) + + def test_non_model_fields(self): + """ + Regression for ensuring ModelAdmin.fields can contain non-model fields + that broke with r11737 + """ + class SongForm(forms.ModelForm): + extra_data = forms.CharField() + class Meta: + model = Song + + class FieldsOnFormOnlyAdmin(admin.ModelAdmin): + form = SongForm + fields = ['title', 'extra_data'] + + validate(FieldsOnFormOnlyAdmin, Song) diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index a2700ba7476f..c492b98ab7c5 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -10,6 +10,7 @@ from django.db import models from django import forms from django.forms.models import BaseModelFormSet +from django.contrib.auth.models import User from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType @@ -91,7 +92,7 @@ class ChapterInline(admin.TabularInline): class ArticleAdmin(admin.ModelAdmin): list_display = ('content', 'date', callable_year, 'model_year', 'modeladmin_year') - list_filter = ('date',) + list_filter = ('date', 'section') def changelist_view(self, request): "Test that extra_context works" @@ -150,6 +151,36 @@ def __unicode__(self): class ThingAdmin(admin.ModelAdmin): list_filter = ('color',) +class Actor(models.Model): + name = models.CharField(max_length=50) + age = models.IntegerField() + def __unicode__(self): + return self.name + +class Inquisition(models.Model): + expected = models.BooleanField() + leader = models.ForeignKey(Actor) + country = models.CharField(max_length=20) + + def __unicode__(self): + return u"by %s from %s" % (self.leader, self.country) + +class InquisitionAdmin(admin.ModelAdmin): + list_display = ('leader', 'country', 'expected') + +class Sketch(models.Model): + title = models.CharField(max_length=100) + inquisition = models.ForeignKey(Inquisition, limit_choices_to={'leader__name': 'Palin', + 'leader__age': 27, + 'expected': False, + }) + + def __unicode__(self): + return self.title + +class SketchAdmin(admin.ModelAdmin): + raw_id_fields = ('inquisition',) + class Fabric(models.Model): NG_CHOICES = ( ('Textured', ( @@ -172,6 +203,7 @@ class Person(models.Model): ) name = models.CharField(max_length=100) gender = models.IntegerField(choices=GENDER_CHOICES) + age = models.IntegerField(default=21) alive = models.BooleanField() def __unicode__(self): @@ -200,6 +232,13 @@ def get_changelist_formset(self, request, **kwargs): return super(PersonAdmin, self).get_changelist_formset(request, formset=BasePersonModelFormSet, **kwargs) +class RowLevelChangePermissionModel(models.Model): + name = models.CharField(max_length=100, blank=True) + +class RowLevelChangePermissionModelAdmin(admin.ModelAdmin): + def has_change_permission(self, request, obj=None): + """ Only allow changing objects with even id number """ + return request.user.is_staff and (obj is not None) and (obj.id % 2 == 0) class Persona(models.Model): """ @@ -464,9 +503,12 @@ class LinkInline(admin.TabularInline): class Post(models.Model): - title = models.CharField(max_length=100) - content = models.TextField() - posted = models.DateField(default=datetime.date.today) + title = models.CharField(max_length=100, help_text="Some help text for the title (with unicode ŠĐĆŽćžšđ)") + content = models.TextField(help_text="Some help text for the content (with unicode ŠĐĆŽćžšđ)") + posted = models.DateField( + default=datetime.date.today, + help_text="Some help text for the date (with unicode ŠĐĆŽćžšđ)" + ) public = models.NullBooleanField() def awesomeness_level(self): @@ -579,12 +621,115 @@ class Pizza(models.Model): class PizzaAdmin(admin.ModelAdmin): readonly_fields = ('toppings',) +class Album(models.Model): + owner = models.ForeignKey(User) + title = models.CharField(max_length=30) + +class AlbumAdmin(admin.ModelAdmin): + list_filter = ['title'] + +class Employee(Person): + code = models.CharField(max_length=20) + +class WorkHour(models.Model): + datum = models.DateField() + employee = models.ForeignKey(Employee) + +class WorkHourAdmin(admin.ModelAdmin): + list_display = ('datum', 'employee') + list_filter = ('employee',) + +class Reservation(models.Model): + start_date = models.DateTimeField() + price = models.IntegerField() + + +DRIVER_CHOICES = ( + (u'bill', 'Bill G'), + (u'steve', 'Steve J'), +) + +RESTAURANT_CHOICES = ( + (u'indian', u'A Taste of India'), + (u'thai', u'Thai Pography'), + (u'pizza', u'Pizza Mama'), +) + +class FoodDelivery(models.Model): + reference = models.CharField(max_length=100) + driver = models.CharField(max_length=100, choices=DRIVER_CHOICES, blank=True) + restaurant = models.CharField(max_length=100, choices=RESTAURANT_CHOICES, blank=True) + + class Meta: + unique_together = (("driver", "restaurant"),) + +class FoodDeliveryAdmin(admin.ModelAdmin): + list_display=('reference', 'driver', 'restaurant') + list_editable = ('driver', 'restaurant') + +class Paper(models.Model): + title = models.CharField(max_length=30) + author = models.CharField(max_length=30, blank=True, null=True) + +class CoverLetter(models.Model): + author = models.CharField(max_length=30) + date_written = models.DateField(null=True, blank=True) + + def __unicode__(self): + return self.author + +class PaperAdmin(admin.ModelAdmin): + """ + A ModelAdin with a custom queryset() method that uses only(), to test + verbose_name display in messages shown after adding Paper instances. + """ + + def queryset(self, request): + return super(PaperAdmin, self).queryset(request).only('title') + +class CoverLetterAdmin(admin.ModelAdmin): + """ + A ModelAdin with a custom queryset() method that uses only(), to test + verbose_name display in messages shown after adding CoverLetter instances. + Note that the CoverLetter model defines a __unicode__ method. + """ + + def queryset(self, request): + #return super(CoverLetterAdmin, self).queryset(request).only('author') + return super(CoverLetterAdmin, self).queryset(request).defer('date_written') + +class Story(models.Model): + title = models.CharField(max_length=100) + content = models.TextField() + +class StoryForm(forms.ModelForm): + class Meta: + widgets = {'title': forms.HiddenInput} + +class StoryAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'content') + list_display_links = ('title',) # 'id' not in list_display_links + list_editable = ('content', ) + form = StoryForm + +class OtherStory(models.Model): + title = models.CharField(max_length=100) + content = models.TextField() + +class OtherStoryAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'content') + list_display_links = ('title', 'id') # 'id' in list_display_links + list_editable = ('content', ) + admin.site.register(Article, ArticleAdmin) admin.site.register(CustomArticle, CustomArticleAdmin) admin.site.register(Section, save_as=True, inlines=[ArticleInline]) admin.site.register(ModelWithStringPrimaryKey) admin.site.register(Color) admin.site.register(Thing, ThingAdmin) +admin.site.register(Actor) +admin.site.register(Inquisition, InquisitionAdmin) +admin.site.register(Sketch, SketchAdmin) admin.site.register(Person, PersonAdmin) admin.site.register(Persona, PersonaAdmin) admin.site.register(Subscriber, SubscriberAdmin) @@ -610,6 +755,14 @@ class PizzaAdmin(admin.ModelAdmin): admin.site.register(PlotDetails) admin.site.register(CyclicOne) admin.site.register(CyclicTwo) +admin.site.register(WorkHour, WorkHourAdmin) +admin.site.register(Reservation) +admin.site.register(FoodDelivery, FoodDeliveryAdmin) +admin.site.register(RowLevelChangePermissionModel, RowLevelChangePermissionModelAdmin) +admin.site.register(Paper, PaperAdmin) +admin.site.register(CoverLetter, CoverLetterAdmin) +admin.site.register(Story, StoryAdmin) +admin.site.register(OtherStory, OtherStoryAdmin) # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. # That way we cover all four cases: @@ -625,3 +778,4 @@ class PizzaAdmin(admin.ModelAdmin): admin.site.register(ChapterXtra1) admin.site.register(Pizza, PizzaAdmin) admin.site.register(Topping) +admin.site.register(Album, AlbumAdmin) diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 1385e5e0aac7..4ed8eb1da7ac 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -2,9 +2,14 @@ import re import datetime +import urlparse + from django.conf import settings +from django.core.exceptions import SuspiciousOperation from django.core.files import temp as tempfile -from django.contrib.auth import admin # Register auth models with the admin. +from django.core.urlresolvers import reverse +# Register auth models with the admin. +from django.contrib.auth import REDIRECT_FIELD_NAME, admin from django.contrib.auth.models import User, Permission, UNUSABLE_PASSWORD from django.contrib.contenttypes.models import ContentType from django.contrib.admin.models import LogEntry, DELETION @@ -17,14 +22,17 @@ from django.utils.cache import get_max_age from django.utils.encoding import iri_to_uri from django.utils.html import escape -from django.utils.translation import get_date_formats, activate, deactivate +from django.utils.translation import activate, deactivate +import django.template.context # local test models -from models import Article, BarAccount, CustomArticle, EmptyModel, \ - FooAccount, Gallery, ModelWithStringPrimaryKey, \ - Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \ - Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \ - Category, Post, Plot, FunkyTag +from models import (Article, BarAccount, CustomArticle, EmptyModel, + FooAccount, Gallery, ModelWithStringPrimaryKey, + Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, + Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, + Category, Post, Plot, FunkyTag, WorkHour, Employee, Inquisition, + Actor, FoodDelivery, RowLevelChangePermissionModel, Paper, CoverLetter, + Story, OtherStory) class AdminViewBasicTest(TestCase): @@ -36,12 +44,18 @@ class AdminViewBasicTest(TestCase): urlbit = 'admin' def setUp(self): - self.old_language_code = settings.LANGUAGE_CODE + self.old_USE_I18N = settings.LANGUAGE_CODE + self.old_USE_L10N = settings.USE_L10N + self.old_LANGUAGE_CODE = settings.LANGUAGE_CODE self.client.login(username='super', password='secret') + settings.USE_I18N = True def tearDown(self): - settings.LANGUAGE_CODE = self.old_language_code + settings.USE_I18N = self.old_USE_I18N + settings.USE_L10N = self.old_USE_L10N + settings.LANGUAGE_CODE = self.old_LANGUAGE_CODE self.client.logout() + formats.reset_format_cache() def testTrailingSlashRequired(self): """ @@ -57,12 +71,12 @@ def testBasicAddGet(self): A smoke test to ensure GET on the add_view works. """ response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit) - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) def testAddWithGETArgs(self): response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'}) - self.failUnlessEqual(response.status_code, 200) - self.failUnless( + self.assertEqual(response.status_code, 200) + self.assertTrue( 'value="My Section"' in response.content, "Couldn't find an input with the right value in the response." ) @@ -72,7 +86,7 @@ def testBasicEditGet(self): A smoke test to ensure GET on the change_view works. """ response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit) - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) def testBasicEditGetStringPK(self): """ @@ -81,7 +95,7 @@ def testBasicEditGetStringPK(self): model with an integer PK field. """ response = self.client.get('/test_admin/%s/admin_views/section/abc/' % self.urlbit) - self.failUnlessEqual(response.status_code, 404) + self.assertEqual(response.status_code, 404) def testBasicAddPost(self): """ @@ -95,7 +109,23 @@ def testBasicAddPost(self): "article_set-MAX_NUM_FORMS": u"0", } response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data) - self.failUnlessEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere + + def testPopupAddPost(self): + """ + Ensure http response from a popup is properly escaped. + """ + post_data = { + '_popup': u'1', + 'title': u'title with a new\nline', + 'content': u'some content', + 'date_0': u'2010-09-10', + 'date_1': u'14:55:39', + } + response = self.client.post('/test_admin/%s/admin_views/article/add/' % self.urlbit, post_data) + self.failUnlessEqual(response.status_code, 200) + self.assertContains(response, 'dismissAddAnotherPopup') + self.assertContains(response, 'title with a new\u000Aline') # Post data for edit inline inline_post_data = { @@ -143,7 +173,7 @@ def testBasicEditPost(self): A smoke test to ensure POST on edit_view works. """ response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data) - self.failUnlessEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testEditSaveAs(self): """ @@ -159,7 +189,7 @@ def testEditSaveAs(self): "article_set-5-section": u"1", }) response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data) - self.failUnlessEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testChangeListSortingCallable(self): """ @@ -167,8 +197,8 @@ def testChangeListSortingCallable(self): (column 2 is callable_year in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2}) - self.failUnlessEqual(response.status_code, 200) - self.failUnless( + self.assertEqual(response.status_code, 200) + self.assertTrue( response.content.index('Oldest content') < response.content.index('Middle content') and response.content.index('Middle content') < response.content.index('Newest content'), "Results of sorting on callable are out of order." @@ -180,8 +210,8 @@ def testChangeListSortingModel(self): (colunn 3 is 'model_year' in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3}) - self.failUnlessEqual(response.status_code, 200) - self.failUnless( + self.assertEqual(response.status_code, 200) + self.assertTrue( response.content.index('Newest content') < response.content.index('Middle content') and response.content.index('Middle content') < response.content.index('Oldest content'), "Results of sorting on Model method are out of order." @@ -193,8 +223,8 @@ def testChangeListSortingModelAdmin(self): (colunn 4 is 'modeladmin_year' in ArticleAdmin) """ response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4}) - self.failUnlessEqual(response.status_code, 200) - self.failUnless( + self.assertEqual(response.status_code, 200) + self.assertTrue( response.content.index('Oldest content') < response.content.index('Middle content') and response.content.index('Middle content') < response.content.index('Newest content'), "Results of sorting on ModelAdmin method are out of order." @@ -203,12 +233,12 @@ def testChangeListSortingModelAdmin(self): def testLimitedFilter(self): """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.""" response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit) - self.failUnlessEqual(response.status_code, 200) - self.failUnless( + self.assertEqual(response.status_code, 200) + self.assertTrue( '
        ' in response.content, "Expected filter not found in changelist view." ) - self.failIf( + self.assertFalse( 'Blue' in response.content, "Changelist filter not correctly limited by limit_choices_to." ) @@ -232,8 +262,8 @@ def testIsNullLookups(self): def testLogoutAndPasswordChangeURLs(self): response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit) - self.failIf('' % self.urlbit not in response.content) - self.failIf('' % self.urlbit not in response.content) + self.assertFalse('' % self.urlbit not in response.content) + self.assertFalse('' % self.urlbit not in response.content) def testNamedGroupFieldChoicesChangeList(self): """ @@ -242,8 +272,8 @@ def testNamedGroupFieldChoicesChangeList(self): has been used in the choices option of a field. """ response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit) - self.failUnlessEqual(response.status_code, 200) - self.failUnless( + self.assertEqual(response.status_code, 200) + self.assertTrue( 'Horizontal' in response.content and 'Vertical' in response.content, "Changelist table isn't showing the right human-readable values set by a model field 'choices' option named group." @@ -255,12 +285,12 @@ def testNamedGroupFieldChoicesFilter(self): been used in the choices option of a model field. """ response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit) - self.failUnlessEqual(response.status_code, 200) - self.failUnless( + self.assertEqual(response.status_code, 200) + self.assertTrue( '
        ' in response.content, "Expected filter not found in changelist view." ) - self.failUnless( + self.assertTrue( 'Horizontal' in response.content and 'Vertical' in response.content, "Changelist filter isn't showing options contained inside a model field 'choices' option named group." @@ -272,7 +302,7 @@ def testChangeListNullBooleanDisplay(self): # against the 'admin2' custom admin (which doesn't have the # Post model). response = self.client.get("/test_admin/admin/admin_views/post/") - self.failUnless('icon-unknown.gif' in response.content) + self.assertTrue('icon-unknown.gif' in response.content) def testI18NLanguageNonEnglishDefault(self): """ @@ -280,22 +310,95 @@ def testI18NLanguageNonEnglishDefault(self): if the default language is non-English but the selected language is English. See #13388 and #3594 for more details. """ - settings.LANGUAGE_CODE = 'fr' - activate('en-us') - response = self.client.get('/test_admin/admin/jsi18n/') - self.assertNotContains(response, 'Choisir une heure') - deactivate() + try: + settings.LANGUAGE_CODE = 'fr' + activate('en-us') + response = self.client.get('/test_admin/admin/jsi18n/') + self.assertNotContains(response, 'Choisir une heure') + finally: + deactivate() def testI18NLanguageNonEnglishFallback(self): """ Makes sure that the fallback language is still working properly in cases where the selected language cannot be found. """ - settings.LANGUAGE_CODE = 'fr' - activate('none') - response = self.client.get('/test_admin/admin/jsi18n/') - self.assertContains(response, 'Choisir une heure') - deactivate() + try: + settings.LANGUAGE_CODE = 'fr' + activate('none') + response = self.client.get('/test_admin/admin/jsi18n/') + self.assertContains(response, 'Choisir une heure') + finally: + deactivate() + + def testL10NDeactivated(self): + """ + Check if L10N is deactivated, the Javascript i18n view doesn't + return localized date/time formats. Refs #14824. + """ + try: + settings.LANGUAGE_CODE = 'ru' + settings.USE_L10N = False + activate('ru') + response = self.client.get('/test_admin/admin/jsi18n/') + self.assertNotContains(response, '%d.%m.%Y %H:%M:%S') + self.assertContains(response, '%Y-%m-%d %H:%M:%S') + finally: + deactivate() + + + def test_disallowed_filtering(self): + self.assertRaises(SuspiciousOperation, + self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy" + ) + + 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.") + + 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') + WorkHour.objects.create(datum=datetime.datetime.now(), employee=e1) + WorkHour.objects.create(datum=datetime.datetime.now(), employee=e2) + response = self.client.get("/test_admin/admin/admin_views/workhour/") + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'employee__person_ptr__exact') + response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk) + self.assertEqual(response.status_code, 200) + + def test_allowed_filtering_15103(self): + """ + Regressions test for ticket 15103 - filtering on fields defined in a + 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") + +class AdminJavaScriptTest(AdminViewBasicTest): + def testSingleWidgetFirsFieldFocus(self): + """ + JavaScript-assisted auto-focus on first field. + """ + response = self.client.get('/test_admin/%s/admin_views/picture/add/' % self.urlbit) + self.assertContains( + response, + '' + ) + + def testMultiWidgetFirsFieldFocus(self): + """ + JavaScript-assisted auto-focus should work if a model/ModelAdmin setup + is such that the first form field has a MultiWidget. + """ + response = self.client.get('/test_admin/%s/admin_views/reservation/add/' % self.urlbit) + self.assertContains( + response, + '' + ) class SaveAsTests(TestCase): @@ -309,7 +412,7 @@ def tearDown(self): def test_save_as_duplication(self): """Ensure save as actually creates a new person""" - post_data = {'_saveasnew':'', 'name':'John M', 'gender':1} + post_data = {'_saveasnew':'', 'name':'John M', 'gender':1, 'age': 42} response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data) self.assertEqual(len(Person.objects.filter(name='John M')), 1) self.assertEqual(len(Person.objects.filter(id=1)), 1) @@ -439,72 +542,73 @@ def testLogin(self): """ # Super User request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_login) self.assertRedirects(login, '/test_admin/admin/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Test if user enters e-mail address request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.super_email_login) self.assertContains(login, "Your e-mail address is not your username") # only correct passwords get a username hint login = self.client.post('/test_admin/admin/', self.super_email_bad_login) - self.assertContains(login, "Usernames cannot contain the '@' character") + self.assertContains(login, "Please enter a correct username and password") new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 login = self.client.post('/test_admin/admin/', self.super_email_login) - self.assertContains(login, "Usernames cannot contain the '@' character") + self.assertContains(login, "Please enter a correct username and password") # Add User request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.adduser_login) self.assertRedirects(login, '/test_admin/admin/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Change User request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.changeuser_login) self.assertRedirects(login, '/test_admin/admin/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Delete User request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.deleteuser_login) self.assertRedirects(login, '/test_admin/admin/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Regular User should not be able to login. request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.joepublic_login) - self.failUnlessEqual(login.status_code, 200) - # Login.context is a list of context dicts we just need to check the first one. - self.assert_(login.context[0].get('error_message')) + self.assertEqual(login.status_code, 200) + self.assertContains(login, "Please enter a correct username and password.") # Requests without username should not return 500 errors. request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/', self.no_username_login) - self.failUnlessEqual(login.status_code, 200) - # Login.context is a list of context dicts we just need to check the first one. + self.assertEqual(login.status_code, 200) + form = login.context[0].get('form') self.assert_(login.context[0].get('error_message')) def testLoginSuccessfullyRedirectsToOriginalUrl(self): request = self.client.get('/test_admin/admin/') - self.failUnlessEqual(request.status_code, 200) - query_string = "the-answer=42" - login = self.client.post('/test_admin/admin/', self.super_login, QUERY_STRING = query_string ) - self.assertRedirects(login, '/test_admin/admin/?%s' % query_string) + self.assertEqual(request.status_code, 200) + query_string = 'the-answer=42' + redirect_url = '/test_admin/admin/?%s' % query_string + new_next = {REDIRECT_FIELD_NAME: redirect_url} + login = self.client.post('/test_admin/admin/', dict(self.super_login, **new_next), QUERY_STRING=query_string) + self.assertRedirects(login, redirect_url) def testAddView(self): """Test add view restricts access and actually adds items.""" @@ -518,38 +622,38 @@ def testAddView(self): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.changeuser_login) # make sure the view removes test cookie - self.failUnlessEqual(self.client.session.test_cookie_worked(), False) + self.assertEqual(self.client.session.test_cookie_worked(), False) request = self.client.get('/test_admin/admin/admin_views/article/add/') - self.failUnlessEqual(request.status_code, 403) + self.assertEqual(request.status_code, 403) # Try POST just to make sure post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) - self.failUnlessEqual(post.status_code, 403) - self.failUnlessEqual(Article.objects.all().count(), 3) + self.assertEqual(post.status_code, 403) + self.assertEqual(Article.objects.all().count(), 3) self.client.get('/test_admin/admin/logout/') # Add user may login and POST to add view, then redirect to admin root self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.adduser_login) addpage = self.client.get('/test_admin/admin/admin_views/article/add/') - self.failUnlessEqual(addpage.status_code, 200) + self.assertEqual(addpage.status_code, 200) change_list_link = 'Articles ›' - self.failIf(change_list_link in addpage.content, + self.assertFalse(change_list_link in addpage.content, 'User restricted to add permission is given link to change list view in breadcrumbs.') post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) self.assertRedirects(post, '/test_admin/admin/') - self.failUnlessEqual(Article.objects.all().count(), 4) + self.assertEqual(Article.objects.all().count(), 4) self.client.get('/test_admin/admin/logout/') # Super can add too, but is redirected to the change list view self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.super_login) addpage = self.client.get('/test_admin/admin/admin_views/article/add/') - self.failUnlessEqual(addpage.status_code, 200) - self.failIf(change_list_link not in addpage.content, + self.assertEqual(addpage.status_code, 200) + self.assertFalse(change_list_link not in addpage.content, 'Unrestricted user is not given link to change list view in breadcrumbs.') post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict) self.assertRedirects(post, '/test_admin/admin/admin_views/article/') - self.failUnlessEqual(Article.objects.all().count(), 5) + self.assertEqual(Article.objects.all().count(), 5) self.client.get('/test_admin/admin/logout/') # 8509 - if a normal user is already logged in, it is possible @@ -559,7 +663,7 @@ def testAddView(self): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.super_login) # make sure the view removes test cookie - self.failUnlessEqual(self.client.session.test_cookie_worked(), False) + self.assertEqual(self.client.session.test_cookie_worked(), False) def testChangeView(self): """Change view should restrict access and allow users to edit items.""" @@ -573,44 +677,80 @@ def testChangeView(self): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.adduser_login) request = self.client.get('/test_admin/admin/admin_views/article/') - self.failUnlessEqual(request.status_code, 403) + self.assertEqual(request.status_code, 403) request = self.client.get('/test_admin/admin/admin_views/article/1/') - self.failUnlessEqual(request.status_code, 403) + self.assertEqual(request.status_code, 403) post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) - self.failUnlessEqual(post.status_code, 403) + self.assertEqual(post.status_code, 403) self.client.get('/test_admin/admin/logout/') # change user can view all items and edit them self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.changeuser_login) request = self.client.get('/test_admin/admin/admin_views/article/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) request = self.client.get('/test_admin/admin/admin_views/article/1/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) self.assertRedirects(post, '/test_admin/admin/admin_views/article/') - self.failUnlessEqual(Article.objects.get(pk=1).content, '

        edited article

        ') + self.assertEqual(Article.objects.get(pk=1).content, '

        edited article

        ') # one error in form should produce singular error message, multiple errors plural change_dict['title'] = '' post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) - self.failUnlessEqual(request.status_code, 200) - self.failUnless('Please correct the error below.' in post.content, + self.assertEqual(request.status_code, 200) + self.assertTrue('Please correct the error below.' in post.content, 'Singular error message not found in response to post with one error.') change_dict['content'] = '' post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict) - self.failUnlessEqual(request.status_code, 200) - self.failUnless('Please correct the errors below.' in post.content, + self.assertEqual(request.status_code, 200) + self.assertTrue('Please correct the errors below.' in post.content, 'Plural error message not found in response to post with multiple errors.') self.client.get('/test_admin/admin/logout/') + # Test redirection when using row-level change permissions. Refs #11513. + RowLevelChangePermissionModel.objects.create(id=1, name="odd id") + RowLevelChangePermissionModel.objects.create(id=2, name="even id") + for login_dict in [self.super_login, self.changeuser_login, self.adduser_login, self.deleteuser_login]: + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', login_dict) + request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/') + self.assertEqual(request.status_code, 403) + request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'}) + self.assertEquals(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id') + self.assertEqual(request.status_code, 403) + request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/') + self.assertEqual(request.status_code, 200) + request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed'}) + self.assertEquals(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed') + self.assertRedirects(request, '/test_admin/admin/') + self.client.get('/test_admin/admin/logout/') + for login_dict in [self.joepublic_login, self.no_username_login]: + self.client.get('/test_admin/admin/') + self.client.post('/test_admin/admin/', login_dict) + request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/') + self.assertEqual(request.status_code, 200) + self.assertContains(request, 'login-form') + request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'}) + self.assertEquals(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id') + self.assertEqual(request.status_code, 200) + self.assertContains(request, 'login-form') + request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/') + self.assertEqual(request.status_code, 200) + self.assertContains(request, 'login-form') + request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed again'}) + self.assertEquals(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed') + self.assertEqual(request.status_code, 200) + self.assertContains(request, 'login-form') + self.client.get('/test_admin/admin/logout/') + def testCustomModelAdminTemplates(self): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.super_login) # Test custom change list template with custom extra context request = self.client.get('/test_admin/admin/admin_views/customarticle/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) self.assert_("var hello = 'Hello!';" in request.content) self.assertTemplateUsed(request, 'custom_admin/change_list.html') @@ -625,13 +765,14 @@ def testCustomModelAdminTemplates(self): 'date_1': '10:54:39' }) self.assertRedirects(post, '/test_admin/admin/admin_views/customarticle/') - self.failUnlessEqual(CustomArticle.objects.all().count(), 1) + self.assertEqual(CustomArticle.objects.all().count(), 1) + article_pk = CustomArticle.objects.all()[0].pk # Test custom delete, change, and object history templates # Test custom change form template - request = self.client.get('/test_admin/admin/admin_views/customarticle/1/') + request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/' % article_pk) self.assertTemplateUsed(request, 'custom_admin/change_form.html') - request = self.client.get('/test_admin/admin/admin_views/customarticle/1/delete/') + request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/delete/' % article_pk) self.assertTemplateUsed(request, 'custom_admin/delete_confirmation.html') request = self.client.post('/test_admin/admin/admin_views/customarticle/', data={ 'index': 0, @@ -639,7 +780,7 @@ def testCustomModelAdminTemplates(self): '_selected_action': ['1'], }) self.assertTemplateUsed(request, 'custom_admin/delete_selected_confirmation.html') - request = self.client.get('/test_admin/admin/admin_views/customarticle/1/history/') + request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/history/' % article_pk) self.assertTemplateUsed(request, 'custom_admin/object_history.html') self.client.get('/test_admin/admin/logout/') @@ -653,10 +794,10 @@ def testDeleteView(self): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.adduser_login) request = self.client.get('/test_admin/admin/admin_views/article/1/delete/') - self.failUnlessEqual(request.status_code, 403) + self.assertEqual(request.status_code, 403) post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict) - self.failUnlessEqual(post.status_code, 403) - self.failUnlessEqual(Article.objects.all().count(), 3) + self.assertEqual(post.status_code, 403) + self.assertEqual(Article.objects.all().count(), 3) self.client.get('/test_admin/admin/logout/') # Delete user can delete @@ -667,13 +808,13 @@ def testDeleteView(self): self.assertContains(response, "admin_views/article/1/") response = self.client.get('/test_admin/admin/admin_views/article/1/delete/') - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict) self.assertRedirects(post, '/test_admin/admin/') - self.failUnlessEqual(Article.objects.all().count(), 2) + self.assertEqual(Article.objects.all().count(), 2) article_ct = ContentType.objects.get_for_model(Article) logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION) - self.failUnlessEqual(logged.object_id, u'1') + self.assertEqual(logged.object_id, u'1') self.client.get('/test_admin/admin/logout/') def testDisabledPermissionsWhenLoggedIn(self): @@ -706,7 +847,7 @@ def test_nesting(self): """ pattern = re.compile(r"""
      • Plot: World Domination\s*
          \s*
        • Plot details: almost finished""") response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1)) - self.failUnless(pattern.search(response.content)) + self.assertTrue(pattern.search(response.content)) def test_cyclic(self): """ @@ -727,7 +868,7 @@ def test_perms_needed(self): delete_user.user_permissions.add(get_perm(Plot, Plot._meta.get_delete_permission())) - self.failUnless(self.client.login(username='deleteuser', + self.assertTrue(self.client.login(username='deleteuser', password='secret')) response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1)) @@ -815,13 +956,13 @@ def test_get_history_view(self): "Retrieving the history for the object using urlencoded form of primary key should work" response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/history/' % quote(self.pk)) self.assertContains(response, escape(self.pk)) - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_get_change_view(self): "Retrieving the object using urlencoded form of primary key should work" response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/' % quote(self.pk)) self.assertContains(response, escape(self.pk)) - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_changelist_to_changeform_link(self): "The link from the changelist referring to the changeform of the object should be quoted" @@ -887,7 +1028,7 @@ def test_url_conflicts_with_history(self): self.assertContains(response, should_contain) -class SecureViewTest(TestCase): +class SecureViewTests(TestCase): fixtures = ['admin-views-users.xml'] def setUp(self): @@ -931,10 +1072,12 @@ def test_secure_view_shows_login_if_not_logged_in(self): def test_secure_view_login_successfully_redirects_to_original_url(self): request = self.client.get('/test_admin/admin/secure-view/') - self.failUnlessEqual(request.status_code, 200) - query_string = "the-answer=42" - login = self.client.post('/test_admin/admin/secure-view/', self.super_login, QUERY_STRING = query_string ) - self.assertRedirects(login, '/test_admin/admin/secure-view/?%s' % query_string) + self.assertEqual(request.status_code, 200) + query_string = 'the-answer=42' + redirect_url = '/test_admin/admin/secure-view/?%s' % query_string + new_next = {REDIRECT_FIELD_NAME: redirect_url} + login = self.client.post('/test_admin/admin/secure-view/', dict(self.super_login, **new_next), QUERY_STRING=query_string) + self.assertRedirects(login, redirect_url) def test_staff_member_required_decorator_works_as_per_admin_login(self): """ @@ -946,57 +1089,57 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self): """ # Super User request = self.client.get('/test_admin/admin/secure-view/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # make sure the view removes test cookie - self.failUnlessEqual(self.client.session.test_cookie_worked(), False) + self.assertEqual(self.client.session.test_cookie_worked(), False) # Test if user enters e-mail address request = self.client.get('/test_admin/admin/secure-view/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) self.assertContains(login, "Your e-mail address is not your username") # only correct passwords get a username hint login = self.client.post('/test_admin/admin/secure-view/', self.super_email_bad_login) - self.assertContains(login, "Usernames cannot contain the '@' character") + self.assertContains(login, "Please enter a correct username and password") new_user = User(username='jondoe', password='secret', email='super@example.com') new_user.save() # check to ensure if there are multiple e-mail addresses a user doesn't get a 500 login = self.client.post('/test_admin/admin/secure-view/', self.super_email_login) - self.assertContains(login, "Usernames cannot contain the '@' character") + self.assertContains(login, "Please enter a correct username and password") # Add User request = self.client.get('/test_admin/admin/secure-view/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.adduser_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Change User request = self.client.get('/test_admin/admin/secure-view/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.changeuser_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Delete User request = self.client.get('/test_admin/admin/secure-view/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.deleteuser_login) self.assertRedirects(login, '/test_admin/admin/secure-view/') - self.failIf(login.context) + self.assertFalse(login.context) self.client.get('/test_admin/admin/logout/') # Regular User should not be able to login. request = self.client.get('/test_admin/admin/secure-view/') - self.failUnlessEqual(request.status_code, 200) + self.assertEqual(request.status_code, 200) login = self.client.post('/test_admin/admin/secure-view/', self.joepublic_login) - self.failUnlessEqual(login.status_code, 200) + self.assertEqual(login.status_code, 200) # Login.context is a list of context dicts we just need to check the first one. self.assert_(login.context[0].get('error_message')) @@ -1007,7 +1150,26 @@ def test_staff_member_required_decorator_works_as_per_admin_login(self): self.client.get('/test_admin/admin/secure-view/') self.client.post('/test_admin/admin/secure-view/', self.super_login) # make sure the view removes test cookie - self.failUnlessEqual(self.client.session.test_cookie_worked(), False) + self.assertEqual(self.client.session.test_cookie_worked(), False) + + def test_shortcut_view_only_available_to_staff(self): + """ + Only admin users should be able to use the admin shortcut view. + """ + user_ctype = ContentType.objects.get_for_model(User) + user = User.objects.get(username='super') + shortcut_url = "/test_admin/admin/r/%s/%s/" % (user_ctype.pk, user.pk) + + # Not logged in: we should see the login page. + response = self.client.get(shortcut_url, follow=False) + self.assertTemplateUsed(response, 'admin/login.html') + + # Logged in? Redirect. + self.client.login(username='super', password='secret') + response = self.client.get(shortcut_url, follow=False) + # Can't use self.assertRedirects() because User.get_absolute_url() is silly. + self.assertEqual(response.status_code, 302) + self.assertEqual(response['Location'], 'http://example.com/users/super/') class AdminViewUnicodeTest(TestCase): fixtures = ['admin-views-unicode.xml'] @@ -1049,7 +1211,7 @@ def testUnicodeEdit(self): } response = self.client.post('/test_admin/admin/admin_views/book/1/', post_data) - self.failUnlessEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere def testUnicodeDelete(self): """ @@ -1057,7 +1219,7 @@ def testUnicodeDelete(self): """ delete_dict = {'post': 'yes'} response = self.client.get('/test_admin/admin/admin_views/book/1/delete/') - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) response = self.client.post('/test_admin/admin/admin_views/book/1/delete/', delete_dict) self.assertRedirects(response, '/test_admin/admin/admin_views/book/') @@ -1075,17 +1237,17 @@ def test_inheritance(self): Podcast.objects.create(name="This Week in Django", release_date=datetime.date.today()) response = self.client.get('/test_admin/admin/admin_views/podcast/') - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_inheritance_2(self): Vodcast.objects.create(name="This Week in Django", released=True) response = self.client.get('/test_admin/admin/admin_views/vodcast/') - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_custom_pk(self): Language.objects.create(iso='en', name='English', english_name='English') response = self.client.get('/test_admin/admin/admin_views/language/') - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) def test_changelist_input_html(self): response = self.client.get('/test_admin/admin/admin_views/person/') @@ -1097,9 +1259,9 @@ def test_changelist_input_html(self): # CSRF field = 1 # field to track 'select all' across paginated views = 1 # 6 + 3 + 4 + 1 + 2 + 1 + 1 = 18 inputs - self.failUnlessEqual(response.content.count("
      • ', 1) + + data = { + "form-TOTAL_FORMS": "3", + "form-INITIAL_FORMS": "3", + "form-MAX_NUM_FORMS": "0", + + "form-0-id": str(fd1.id), + "form-0-reference": "123", + "form-0-driver": "bill", + "form-0-restaurant": "thai", + + # Same data as above: Forbidden because of unique_together! + "form-1-id": str(fd2.id), + "form-1-reference": "456", + "form-1-driver": "bill", + "form-1-restaurant": "thai", + + # Same data also. + "form-2-id": str(fd3.id), + "form-2-reference": "789", + "form-2-driver": "bill", + "form-2-restaurant": "thai", + + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/fooddelivery/', data) + self.assertContains(response, '', 2) def test_non_form_errors(self): # test if non-form errors are handled; ticket #12716 @@ -1255,13 +1478,13 @@ def test_list_editable_ordering(self): } response = self.client.post('/test_admin/admin/admin_views/category/', data) # Successful post will redirect - self.failUnlessEqual(response.status_code, 302) + self.assertEqual(response.status_code, 302) # Check that the order values have been applied to the right objects - self.failUnlessEqual(Category.objects.get(id=1).order, 14) - self.failUnlessEqual(Category.objects.get(id=2).order, 13) - self.failUnlessEqual(Category.objects.get(id=3).order, 1) - self.failUnlessEqual(Category.objects.get(id=4).order, 0) + self.assertEqual(Category.objects.get(id=1).order, 14) + self.assertEqual(Category.objects.get(id=2).order, 13) + self.assertEqual(Category.objects.get(id=3).order, 1) + self.assertEqual(Category.objects.get(id=4).order, 0) def test_list_editable_action_submit(self): # List editable changes should not be executed if the action "Go" button is @@ -1287,8 +1510,8 @@ def test_list_editable_action_submit(self): } self.client.post('/test_admin/admin/admin_views/person/', data) - self.failUnlessEqual(Person.objects.get(name="John Mauchly").alive, True) - self.failUnlessEqual(Person.objects.get(name="Grace Hopper").gender, 1) + self.assertEqual(Person.objects.get(name="John Mauchly").alive, True) + self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 1) def test_list_editable_action_choices(self): # List editable changes should be executed if the "Save" button is @@ -1314,11 +1537,41 @@ def test_list_editable_action_choices(self): } self.client.post('/test_admin/admin/admin_views/person/', data) - self.failUnlessEqual(Person.objects.get(name="John Mauchly").alive, False) - self.failUnlessEqual(Person.objects.get(name="Grace Hopper").gender, 2) + self.assertEqual(Person.objects.get(name="John Mauchly").alive, False) + self.assertEqual(Person.objects.get(name="Grace Hopper").gender, 2) + def test_pk_hidden_fields(self): + """ Ensure that hidden pk fields aren't displayed in the table body and + that their corresponding human-readable value is displayed instead. + Note that the hidden pk fields are in fact be displayed but + separately (not in the table), and only once. + Refs #12475. + """ + story1 = Story.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...') + story2 = Story.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...') + response = self.client.get('/test_admin/admin/admin_views/story/') + self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. + self.assertContains(response, 'id="id_form-1-id"', 1) + self.assertContains(response, '
        \n\n
        ' % (story2.id, story1.id)) + self.assertContains(response, '
        ' % story1.id, 1) + self.assertContains(response, '' % story2.id, 1) + + def test_pk_hidden_fields_with_list_display_links(self): + """ Similarly as test_pk_hidden_fields, but when the hidden pk fields are + referenced in list_display_links. + Refs #12475. + """ + story1 = OtherStory.objects.create(title='The adventures of Guido', content='Once upon a time in Djangoland...') + story2 = OtherStory.objects.create(title='Crouching Tiger, Hidden Python', content='The Python was sneaking into...') + response = self.client.get('/test_admin/admin/admin_views/otherstory/') + self.assertContains(response, 'id="id_form-0-id"', 1) # Only one hidden field, in a separate place than the table. + self.assertContains(response, 'id="id_form-1-id"', 1) + self.assertContains(response, '
        \n\n
        ' % (story2.id, story1.id)) + self.assertContains(response, '
        ' % (story1.id, story1.id), 1) + self.assertContains(response, '' % (story2.id, story2.id), 1) + class AdminSearchTest(TestCase): fixtures = ['admin-views-users','multiple-child-classes'] @@ -1335,6 +1588,16 @@ def test_search_on_sibling_models(self): # confirm the search returned 1 object self.assertContains(response, "\n1 recommendation\n") + def test_with_fk_to_field(self): + """Ensure that the to_field GET parameter is preserved when a search + is performed. Refs #10918. + """ + from django.contrib.admin.views.main import TO_FIELD_VAR + response = self.client.get('/test_admin/admin/auth/user/?q=joe&%s=username' % TO_FIELD_VAR) + self.assertContains(response, "\n1 user\n") + self.assertContains(response, '') + + class AdminInheritedInlinesTest(TestCase): fixtures = ['admin-views-users.xml',] @@ -1356,7 +1619,7 @@ def testInline(self): response = self.client.get('/test_admin/admin/admin_views/persona/add/') names = name_re.findall(response.content) # make sure we have no duplicate HTML names - self.failUnlessEqual(len(names), len(set(names))) + self.assertEqual(len(names), len(set(names))) # test the add case post_data = { @@ -1373,20 +1636,24 @@ def testInline(self): } response = self.client.post('/test_admin/admin/admin_views/persona/add/', post_data) - self.failUnlessEqual(response.status_code, 302) # redirect somewhere - self.failUnlessEqual(Persona.objects.count(), 1) - self.failUnlessEqual(FooAccount.objects.count(), 1) - self.failUnlessEqual(BarAccount.objects.count(), 1) - self.failUnlessEqual(FooAccount.objects.all()[0].username, foo_user) - self.failUnlessEqual(BarAccount.objects.all()[0].username, bar_user) - self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2) + self.assertEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(Persona.objects.count(), 1) + self.assertEqual(FooAccount.objects.count(), 1) + self.assertEqual(BarAccount.objects.count(), 1) + self.assertEqual(FooAccount.objects.all()[0].username, foo_user) + self.assertEqual(BarAccount.objects.all()[0].username, bar_user) + self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) + + persona_id = Persona.objects.all()[0].id + foo_id = FooAccount.objects.all()[0].id + bar_id = BarAccount.objects.all()[0].id # test the edit case - response = self.client.get('/test_admin/admin/admin_views/persona/1/') + response = self.client.get('/test_admin/admin/admin_views/persona/%d/' % persona_id) names = name_re.findall(response.content) # make sure we have no duplicate HTML names - self.failUnlessEqual(len(names), len(set(names))) + self.assertEqual(len(names), len(set(names))) post_data = { "name": u"Test Name", @@ -1396,25 +1663,25 @@ def testInline(self): "accounts-MAX_NUM_FORMS": u"0", "accounts-0-username": "%s-1" % foo_user, - "accounts-0-account_ptr": "1", - "accounts-0-persona": "1", + "accounts-0-account_ptr": str(foo_id), + "accounts-0-persona": str(persona_id), "accounts-2-TOTAL_FORMS": u"2", "accounts-2-INITIAL_FORMS": u"1", "accounts-2-MAX_NUM_FORMS": u"0", "accounts-2-0-username": "%s-1" % bar_user, - "accounts-2-0-account_ptr": "2", - "accounts-2-0-persona": "1", + "accounts-2-0-account_ptr": str(bar_id), + "accounts-2-0-persona": str(persona_id), } - response = self.client.post('/test_admin/admin/admin_views/persona/1/', post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Persona.objects.count(), 1) - self.failUnlessEqual(FooAccount.objects.count(), 1) - self.failUnlessEqual(BarAccount.objects.count(), 1) - self.failUnlessEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user) - self.failUnlessEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) - self.failUnlessEqual(Persona.objects.all()[0].accounts.count(), 2) + response = self.client.post('/test_admin/admin/admin_views/persona/%d/' % persona_id, post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Persona.objects.count(), 1) + self.assertEqual(FooAccount.objects.count(), 1) + self.assertEqual(BarAccount.objects.count(), 1) + self.assertEqual(FooAccount.objects.all()[0].username, "%s-1" % foo_user) + self.assertEqual(BarAccount.objects.all()[0].username, "%s-1" % bar_user) + self.assertEqual(Persona.objects.all()[0].accounts.count(), 2) from django.core import mail @@ -1452,9 +1719,9 @@ def test_model_admin_default_delete_action(self): } confirmation = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) self.assertContains(confirmation, "Are you sure you want to delete the selected subscriber objects") - self.failUnless(confirmation.content.count(ACTION_CHECKBOX_NAME) == 2) + self.assertTrue(confirmation.content.count(ACTION_CHECKBOX_NAME) == 2) response = self.client.post('/test_admin/admin/admin_views/subscriber/', delete_confirmation_data) - self.failUnlessEqual(Subscriber.objects.count(), 0) + self.assertEqual(Subscriber.objects.count(), 0) def test_custom_function_mail_action(self): "Tests a custom action defined in a function" @@ -1475,7 +1742,22 @@ def test_custom_function_action_with_redirect(self): 'index': 0, } response = self.client.post('/test_admin/admin/admin_views/externalsubscriber/', action_data) - self.failUnlessEqual(response.status_code, 302) + self.assertEqual(response.status_code, 302) + + def test_default_redirect(self): + """ + Test that actions which don't return an HttpResponse are redirected to + the same page, retaining the querystring (which may contain changelist + information). + """ + action_data = { + ACTION_CHECKBOX_NAME: [1], + 'action' : 'external_mail', + 'index': 0, + } + url = '/test_admin/admin/admin_views/externalsubscriber/?ot=asc&o=1' + response = self.client.post(url, action_data) + self.assertRedirects(response, url) def test_model_without_action(self): "Tests a ModelAdmin without any action" @@ -1499,7 +1781,7 @@ def test_model_without_action_still_has_jquery(self): def test_action_column_class(self): "Tests that the checkbox column class is present in the response" response = self.client.get('/test_admin/admin/admin_views/subscriber/') - self.assertNotEquals(response.context["action_form"], None) + self.assertNotEqual(response.context["action_form"], None) self.assert_('action-checkbox-column' in response.content, "Expected an action-checkbox-column in response") @@ -1532,7 +1814,7 @@ def test_user_message_on_none_selected(self): response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) msg = """Items must be selected in order to perform actions on them. No items have been changed.""" self.assertContains(response, msg) - self.failUnlessEqual(Subscriber.objects.count(), 2) + self.assertEqual(Subscriber.objects.count(), 2) def test_user_message_on_no_action(self): """ @@ -1546,7 +1828,7 @@ def test_user_message_on_no_action(self): response = self.client.post('/test_admin/admin/admin_views/subscriber/', action_data) msg = """No action selected.""" self.assertContains(response, msg) - self.failUnlessEqual(Subscriber.objects.count(), 2) + self.assertEqual(Subscriber.objects.count(), 2) def test_selection_counter(self): """ @@ -1562,7 +1844,7 @@ class TestCustomChangeList(TestCase): def setUp(self): result = self.client.login(username='super', password='secret') - self.failUnlessEqual(result, True) + self.assertEqual(result, True) def tearDown(self): self.client.logout() @@ -1574,12 +1856,12 @@ def test_custom_changelist(self): # Insert some data post_data = {"name": u"First Gadget"} response = self.client.post('/test_admin/%s/admin_views/gadget/add/' % self.urlbit, post_data) - self.failUnlessEqual(response.status_code, 302) # redirect somewhere + self.assertEqual(response.status_code, 302) # redirect somewhere # Hit the page once to get messages out of the queue message list response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) # Ensure that that data is still not visible on the page response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) self.assertNotContains(response, 'First Gadget') @@ -1588,7 +1870,7 @@ class TestInlineNotEditable(TestCase): def setUp(self): result = self.client.login(username='super', password='secret') - self.failUnlessEqual(result, True) + self.assertEqual(result, True) def tearDown(self): self.client.logout() @@ -1598,7 +1880,7 @@ def test(self): InlineModelAdmin broken? """ response = self.client.get('/test_admin/admin/admin_views/parent/add/') - self.failUnlessEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200) class AdminCustomQuerysetTest(TestCase): fixtures = ['admin-views-users.xml'] @@ -1623,6 +1905,37 @@ def test_change_view(self): else: self.assertEqual(response.status_code, 404) + def test_add_model_modeladmin_only_qs(self): + # only() is used in ModelAdmin.queryset() + p = Paper.objects.create(title=u"My Paper Title") + self.assertEqual(Paper.objects.count(), 1) + response = self.client.get('/test_admin/admin/admin_views/paper/%s/' % p.pk) + self.assertEqual(response.status_code, 200) + post_data = { + "title": u"My Modified Paper Title", + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/paper/%s/' % p.pk, + post_data, follow=True) + self.assertEqual(response.status_code, 200) + # Message should contain non-ugly model name. Instance representation is set by unicode() (ugly) + self.assertContains(response, '
      • The paper "Paper_Deferred_author object" was changed successfully.
      • ') + + # defer() is used in ModelAdmin.queryset() + cl = CoverLetter.objects.create(author=u"John Doe") + self.assertEqual(CoverLetter.objects.count(), 1) + response = self.client.get('/test_admin/admin/admin_views/coverletter/%s/' % cl.pk) + self.assertEqual(response.status_code, 200) + post_data = { + "author": u"John Doe II", + "_save": "Save", + } + response = self.client.post('/test_admin/admin/admin_views/coverletter/%s/' % cl.pk, + post_data, follow=True) + self.assertEqual(response.status_code, 200) + # Message should contain non-ugly model name. Instance representation is set by model's __unicode__() + self.assertContains(response, '
      • The cover letter "John Doe II" was changed successfully.
      • ') + class AdminInlineFileUploadTest(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-actions.xml'] urlbit = 'admin' @@ -1638,10 +1951,10 @@ def setUp(self): file1.write('a' * (2 ** 21)) filename = file1.name file1.close() - g = Gallery(name="Test Gallery") - g.save() - p = Picture(name="Test Picture", image=filename, gallery=g) - p.save() + self.gallery = Gallery(name="Test Gallery") + self.gallery.save() + self.picture = Picture(name="Test Picture", image=filename, gallery=self.gallery) + self.picture.save() def tearDown(self): self.client.logout() @@ -1655,17 +1968,17 @@ def test_inline_file_upload_edit_validation_error_post(self): "pictures-TOTAL_FORMS": u"2", "pictures-INITIAL_FORMS": u"1", "pictures-MAX_NUM_FORMS": u"0", - "pictures-0-id": u"1", - "pictures-0-gallery": u"1", + "pictures-0-id": unicode(self.picture.id), + "pictures-0-gallery": unicode(self.gallery.id), "pictures-0-name": "Test Picture", "pictures-0-image": "", "pictures-1-id": "", - "pictures-1-gallery": "1", + "pictures-1-gallery": str(self.gallery.id), "pictures-1-name": "Test Picture 2", "pictures-1-image": "", } - response = self.client.post('/test_admin/%s/admin_views/gallery/1/' % self.urlbit, post_data) - self.failUnless(response._container[0].find("Currently:") > -1) + response = self.client.post('/test_admin/%s/admin_views/gallery/%d/' % (self.urlbit, self.gallery.id), post_data) + self.assertTrue(response._container[0].find("Currently:") > -1) class AdminInlineTests(TestCase): @@ -1758,7 +2071,7 @@ def setUp(self): } result = self.client.login(username='super', password='secret') - self.failUnlessEqual(result, True) + self.assertEqual(result, True) self.collector = Collector(pk=1,name='John Fowles') self.collector.save() @@ -1769,95 +2082,99 @@ def test_simple_inline(self): "A simple model can be saved as inlines" # First add a new inline self.post_data['widget_set-0-name'] = "Widget 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Widget.objects.count(), 1) - self.failUnlessEqual(Widget.objects.all()[0].name, "Widget 1") + collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Widget.objects.count(), 1) + self.assertEqual(Widget.objects.all()[0].name, "Widget 1") + widget_id = Widget.objects.all()[0].id # Check that the PK link exists on the rendered form - response = self.client.get('/test_admin/admin/admin_views/collector/1/') + response = self.client.get(collector_url) self.assertContains(response, 'name="widget_set-0-id"') # Now resave that inline self.post_data['widget_set-INITIAL_FORMS'] = "1" - self.post_data['widget_set-0-id'] = "1" + self.post_data['widget_set-0-id'] = str(widget_id) self.post_data['widget_set-0-name'] = "Widget 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Widget.objects.count(), 1) - self.failUnlessEqual(Widget.objects.all()[0].name, "Widget 1") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Widget.objects.count(), 1) + self.assertEqual(Widget.objects.all()[0].name, "Widget 1") # Now modify that inline self.post_data['widget_set-INITIAL_FORMS'] = "1" - self.post_data['widget_set-0-id'] = "1" + self.post_data['widget_set-0-id'] = str(widget_id) self.post_data['widget_set-0-name'] = "Widget 1 Updated" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Widget.objects.count(), 1) - self.failUnlessEqual(Widget.objects.all()[0].name, "Widget 1 Updated") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Widget.objects.count(), 1) + self.assertEqual(Widget.objects.all()[0].name, "Widget 1 Updated") def test_explicit_autofield_inline(self): "A model with an explicit autofield primary key can be saved as inlines. Regression for #8093" # First add a new inline self.post_data['grommet_set-0-name'] = "Grommet 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Grommet.objects.count(), 1) - self.failUnlessEqual(Grommet.objects.all()[0].name, "Grommet 1") + collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Grommet.objects.count(), 1) + self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1") # Check that the PK link exists on the rendered form - response = self.client.get('/test_admin/admin/admin_views/collector/1/') + response = self.client.get(collector_url) self.assertContains(response, 'name="grommet_set-0-code"') # Now resave that inline self.post_data['grommet_set-INITIAL_FORMS'] = "1" - self.post_data['grommet_set-0-code'] = "1" + self.post_data['grommet_set-0-code'] = str(Grommet.objects.all()[0].code) self.post_data['grommet_set-0-name'] = "Grommet 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Grommet.objects.count(), 1) - self.failUnlessEqual(Grommet.objects.all()[0].name, "Grommet 1") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Grommet.objects.count(), 1) + self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1") # Now modify that inline self.post_data['grommet_set-INITIAL_FORMS'] = "1" - self.post_data['grommet_set-0-code'] = "1" + self.post_data['grommet_set-0-code'] = str(Grommet.objects.all()[0].code) self.post_data['grommet_set-0-name'] = "Grommet 1 Updated" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Grommet.objects.count(), 1) - self.failUnlessEqual(Grommet.objects.all()[0].name, "Grommet 1 Updated") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(Grommet.objects.count(), 1) + self.assertEqual(Grommet.objects.all()[0].name, "Grommet 1 Updated") def test_char_pk_inline(self): "A model with a character PK can be saved as inlines. Regression for #10992" # First add a new inline self.post_data['doohickey_set-0-code'] = "DH1" self.post_data['doohickey_set-0-name'] = "Doohickey 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(DooHickey.objects.count(), 1) - self.failUnlessEqual(DooHickey.objects.all()[0].name, "Doohickey 1") + collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(DooHickey.objects.count(), 1) + self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1") # Check that the PK link exists on the rendered form - response = self.client.get('/test_admin/admin/admin_views/collector/1/') + response = self.client.get(collector_url) self.assertContains(response, 'name="doohickey_set-0-code"') # Now resave that inline self.post_data['doohickey_set-INITIAL_FORMS'] = "1" self.post_data['doohickey_set-0-code'] = "DH1" self.post_data['doohickey_set-0-name'] = "Doohickey 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(DooHickey.objects.count(), 1) - self.failUnlessEqual(DooHickey.objects.all()[0].name, "Doohickey 1") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(DooHickey.objects.count(), 1) + self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1") # Now modify that inline self.post_data['doohickey_set-INITIAL_FORMS'] = "1" self.post_data['doohickey_set-0-code'] = "DH1" self.post_data['doohickey_set-0-name'] = "Doohickey 1 Updated" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(DooHickey.objects.count(), 1) - self.failUnlessEqual(DooHickey.objects.all()[0].name, "Doohickey 1 Updated") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(DooHickey.objects.count(), 1) + self.assertEqual(DooHickey.objects.all()[0].name, "Doohickey 1 Updated") def test_integer_pk_inline(self): "A model with an integer PK can be saved as inlines. Regression for #10992" @@ -1865,9 +2182,9 @@ def test_integer_pk_inline(self): self.post_data['whatsit_set-0-index'] = "42" self.post_data['whatsit_set-0-name'] = "Whatsit 1" response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Whatsit.objects.count(), 1) - self.failUnlessEqual(Whatsit.objects.all()[0].name, "Whatsit 1") + self.assertEqual(response.status_code, 302) + self.assertEqual(Whatsit.objects.count(), 1) + self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1") # Check that the PK link exists on the rendered form response = self.client.get('/test_admin/admin/admin_views/collector/1/') @@ -1878,49 +2195,51 @@ def test_integer_pk_inline(self): self.post_data['whatsit_set-0-index'] = "42" self.post_data['whatsit_set-0-name'] = "Whatsit 1" response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Whatsit.objects.count(), 1) - self.failUnlessEqual(Whatsit.objects.all()[0].name, "Whatsit 1") + self.assertEqual(response.status_code, 302) + self.assertEqual(Whatsit.objects.count(), 1) + self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1") # Now modify that inline self.post_data['whatsit_set-INITIAL_FORMS'] = "1" self.post_data['whatsit_set-0-index'] = "42" self.post_data['whatsit_set-0-name'] = "Whatsit 1 Updated" response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(Whatsit.objects.count(), 1) - self.failUnlessEqual(Whatsit.objects.all()[0].name, "Whatsit 1 Updated") + self.assertEqual(response.status_code, 302) + self.assertEqual(Whatsit.objects.count(), 1) + self.assertEqual(Whatsit.objects.all()[0].name, "Whatsit 1 Updated") def test_inherited_inline(self): "An inherited model can be saved as inlines. Regression for #11042" # First add a new inline self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(FancyDoodad.objects.count(), 1) - self.failUnlessEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1") + collector_url = '/test_admin/admin/admin_views/collector/%d/' % self.collector.pk + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(FancyDoodad.objects.count(), 1) + self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1") + doodad_pk = FancyDoodad.objects.all()[0].pk # Check that the PK link exists on the rendered form - response = self.client.get('/test_admin/admin/admin_views/collector/1/') + response = self.client.get(collector_url) self.assertContains(response, 'name="fancydoodad_set-0-doodad_ptr"') # Now resave that inline self.post_data['fancydoodad_set-INITIAL_FORMS'] = "1" - self.post_data['fancydoodad_set-0-doodad_ptr'] = "1" + self.post_data['fancydoodad_set-0-doodad_ptr'] = str(doodad_pk) self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(FancyDoodad.objects.count(), 1) - self.failUnlessEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(FancyDoodad.objects.count(), 1) + self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1") # Now modify that inline self.post_data['fancydoodad_set-INITIAL_FORMS'] = "1" - self.post_data['fancydoodad_set-0-doodad_ptr'] = "1" + self.post_data['fancydoodad_set-0-doodad_ptr'] = str(doodad_pk) self.post_data['fancydoodad_set-0-name'] = "Fancy Doodad 1 Updated" - response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) - self.failUnlessEqual(response.status_code, 302) - self.failUnlessEqual(FancyDoodad.objects.count(), 1) - self.failUnlessEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1 Updated") + response = self.client.post(collector_url, self.post_data) + self.assertEqual(response.status_code, 302) + self.assertEqual(FancyDoodad.objects.count(), 1) + self.assertEqual(FancyDoodad.objects.all()[0].name, "Fancy Doodad 1 Updated") def test_ordered_inline(self): """Check that an inline with an editable ordering fields is @@ -1969,14 +2288,14 @@ def test_ordered_inline(self): }) response = self.client.post('/test_admin/admin/admin_views/collector/1/', self.post_data) # Successful post will redirect - self.failUnlessEqual(response.status_code, 302) + self.assertEqual(response.status_code, 302) # Check that the order values have been applied to the right objects - self.failUnlessEqual(self.collector.category_set.count(), 4) - self.failUnlessEqual(Category.objects.get(id=1).order, 14) - self.failUnlessEqual(Category.objects.get(id=2).order, 13) - self.failUnlessEqual(Category.objects.get(id=3).order, 1) - self.failUnlessEqual(Category.objects.get(id=4).order, 0) + self.assertEqual(self.collector.category_set.count(), 4) + self.assertEqual(Category.objects.get(id=1).order, 14) + self.assertEqual(Category.objects.get(id=2).order, 13) + self.assertEqual(Category.objects.get(id=3).order, 1) + self.assertEqual(Category.objects.get(id=4).order, 0) class NeverCacheTests(TestCase): @@ -1991,64 +2310,64 @@ def tearDown(self): def testAdminIndex(self): "Check the never-cache status of the main index" response = self.client.get('/test_admin/admin/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testAppIndex(self): "Check the never-cache status of an application index" response = self.client.get('/test_admin/admin/admin_views/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testModelIndex(self): "Check the never-cache status of a model index" response = self.client.get('/test_admin/admin/admin_views/fabric/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testModelAdd(self): "Check the never-cache status of a model add page" response = self.client.get('/test_admin/admin/admin_views/fabric/add/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testModelView(self): "Check the never-cache status of a model edit page" response = self.client.get('/test_admin/admin/admin_views/section/1/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testModelHistory(self): "Check the never-cache status of a model history page" response = self.client.get('/test_admin/admin/admin_views/section/1/history/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testModelDelete(self): "Check the never-cache status of a model delete page" response = self.client.get('/test_admin/admin/admin_views/section/1/delete/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testLogin(self): "Check the never-cache status of login views" self.client.logout() response = self.client.get('/test_admin/admin/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testLogout(self): "Check the never-cache status of logout view" response = self.client.get('/test_admin/admin/logout/') - self.failUnlessEqual(get_max_age(response), 0) + self.assertEqual(get_max_age(response), 0) def testPasswordChange(self): "Check the never-cache status of the password change view" self.client.logout() response = self.client.get('/test_admin/password_change/') - self.failUnlessEqual(get_max_age(response), None) + self.assertEqual(get_max_age(response), None) def testPasswordChangeDone(self): "Check the never-cache status of the password change done view" response = self.client.get('/test_admin/admin/password_change/done/') - self.failUnlessEqual(get_max_age(response), None) + self.assertEqual(get_max_age(response), None) def testJsi18n(self): "Check the never-cache status of the Javascript i18n view" response = self.client.get('/test_admin/admin/jsi18n/') - self.failUnlessEqual(get_max_age(response), None) + self.assertEqual(get_max_age(response), None) class ReadonlyTest(TestCase): @@ -2082,6 +2401,10 @@ def test_readonly_get(self): self.assertContains(response, '
        ') self.assertContains(response, '
        ') self.assertContains(response, '
        ') + self.assertContains(response, '

        ', 3) + self.assertContains(response, '

        Some help text for the title (with unicode ŠĐĆŽćžšđ)

        ') + self.assertContains(response, '

        Some help text for the content (with unicode ŠĐĆŽćžšđ)

        ') + self.assertContains(response, '

        Some help text for the date (with unicode ŠĐĆŽćžšđ)

        ') p = Post.objects.create(title="I worked on readonly_fields", content="Its good stuff") response = self.client.get('/test_admin/admin/admin_views/post/%d/' % p.pk) @@ -2113,11 +2436,42 @@ def test_readonly_manytomany(self): response = self.client.get('/test_admin/admin/admin_views/pizza/add/') self.assertEqual(response.status_code, 200) -class IncompleteFormTest(TestCase): + +class RawIdFieldsTest(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + 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, + country="England") + inquisition2 = Inquisition.objects.create(expected=False, + leader=actor, + country="Spain") + response = self.client.get('/test_admin/admin/admin_views/sketch/add/') + # Find the link + m = re.search(r']* id="lookup_id_inquisition"', response.content) + self.assertTrue(m) # Got a match + popup_url = m.groups()[0].replace("&", "&") + + # Handle relative links + popup_url = urlparse.urljoin(response.request['PATH_INFO'], popup_url) + # Get the popup + response2 = self.client.get(popup_url) + self.assertContains(response2, "Spain") + self.assertNotContains(response2, "England") + +class UserAdminTest(TestCase): """ - Tests validation of a ModelForm that doesn't explicitly have all data - corresponding to model fields. Model validation shouldn't fail - such a forms. + Tests user CRUD functionality. """ fixtures = ['admin-views-users.xml'] @@ -2127,7 +2481,20 @@ def setUp(self): def tearDown(self): self.client.logout() - def test_user_creation(self): + def test_save_button(self): + user_count = User.objects.count() + response = self.client.post('/test_admin/admin/auth/user/add/', { + 'username': 'newuser', + 'password1': 'newpassword', + 'password2': 'newpassword', + }) + new_user = User.objects.order_by('-id')[0] + self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) + self.assertEqual(User.objects.count(), user_count + 1) + self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) + + def test_save_continue_editing_button(self): + user_count = User.objects.count() response = self.client.post('/test_admin/admin/auth/user/add/', { 'username': 'newuser', 'password1': 'newpassword', @@ -2136,7 +2503,8 @@ def test_user_creation(self): }) new_user = User.objects.order_by('-id')[0] self.assertRedirects(response, '/test_admin/admin/auth/user/%s/' % new_user.pk) - self.assertNotEquals(new_user.password, UNUSABLE_PASSWORD) + self.assertEqual(User.objects.count(), user_count + 1) + self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) def test_password_mismatch(self): response = self.client.post('/test_admin/admin/auth/user/add/', { @@ -2144,8 +2512,232 @@ def test_password_mismatch(self): 'password1': 'newpassword', 'password2': 'mismatch', }) - self.assertEquals(response.status_code, 200) + self.assertEqual(response.status_code, 200) adminform = response.context['adminform'] - self.assert_('password' not in adminform.form.errors) - self.assertEquals(adminform.form.errors['password2'], + self.assertTrue('password' not in adminform.form.errors) + self.assertEqual(adminform.form.errors['password2'], [u"The two password fields didn't match."]) + + def test_user_fk_popup(self): + """Quick user addition in a FK popup shouldn't invoke view for further user customization""" + response = self.client.get('/test_admin/admin/admin_views/album/add/') + self.assertEqual(response.status_code, 200) + self.assertContains(response, '/test_admin/admin/auth/user/add') + self.assertContains(response, 'class="add-another" id="add_id_owner" onclick="return showAddAnotherPopup(this);"') + response = self.client.get('/test_admin/admin/auth/user/add/?_popup=1') + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, 'name="_continue"') + self.assertNotContains(response, 'name="_addanother"') + data = { + 'username': 'newuser', + 'password1': 'newpassword', + 'password2': 'newpassword', + '_popup': '1', + '_save': '1', + } + response = self.client.post('/test_admin/admin/auth/user/add/?_popup=1', data, follow=True) + self.assertEqual(response.status_code, 200) + self.assertContains(response, 'dismissAddAnotherPopup') + + def test_save_add_another_button(self): + user_count = User.objects.count() + response = self.client.post('/test_admin/admin/auth/user/add/', { + 'username': 'newuser', + 'password1': 'newpassword', + 'password2': 'newpassword', + '_addanother': '1', + }) + new_user = User.objects.order_by('-id')[0] + self.assertRedirects(response, '/test_admin/admin/auth/user/add/') + self.assertEqual(User.objects.count(), user_count + 1) + self.assertNotEqual(new_user.password, UNUSABLE_PASSWORD) + +try: + # If docutils isn't installed, skip the AdminDocs tests. + import docutils + + class AdminDocsTest(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + + def test_tags(self): + response = self.client.get('/test_admin/admin/doc/tags/') + + # The builtin tag group exists + self.assertContains(response, "

        Built-in tags

        ", count=2) + + # A builtin tag exists in both the index and detail + self.assertContains(response, '

        autoescape

        ') + self.assertContains(response, '
      • autoescape
      • ') + + # An app tag exists in both the index and detail + self.assertContains(response, '

        get_comment_count

        ') + self.assertContains(response, '
      • get_comment_count
      • ') + + # The admin list tag group exists + self.assertContains(response, "

        admin_list

        ", count=2) + + # An admin list tag exists in both the index and detail + self.assertContains(response, '

        admin_actions

        ') + self.assertContains(response, '
      • admin_actions
      • ') + + def test_filters(self): + response = self.client.get('/test_admin/admin/doc/filters/') + + # The builtin filter group exists + self.assertContains(response, "

        Built-in filters

        ", count=2) + + # A builtin filter exists in both the index and detail + self.assertContains(response, '

        add

        ') + self.assertContains(response, '
      • add
      • ') + +except ImportError: + pass + +class ValidXHTMLTests(TestCase): + fixtures = ['admin-views-users.xml'] + urlbit = 'admin' + + def setUp(self): + self._context_processors = None + self._use_i18n, settings.USE_I18N = settings.USE_I18N, False + if 'django.core.context_processors.i18n' in settings.TEMPLATE_CONTEXT_PROCESSORS: + self._context_processors = settings.TEMPLATE_CONTEXT_PROCESSORS + cp = list(settings.TEMPLATE_CONTEXT_PROCESSORS) + cp.remove('django.core.context_processors.i18n') + settings.TEMPLATE_CONTEXT_PROCESSORS = tuple(cp) + # Force re-evaluation of the contex processor list + django.template.context._standard_context_processors = None + self.client.login(username='super', password='secret') + + def tearDown(self): + self.client.logout() + if self._context_processors is not None: + settings.TEMPLATE_CONTEXT_PROCESSORS = self._context_processors + # Force re-evaluation of the contex processor list + django.template.context._standard_context_processors = None + settings.USE_I18N = self._use_i18n + + def testLangNamePresent(self): + response = self.client.get('/test_admin/%s/admin_views/' % self.urlbit) + self.assertFalse(' lang=""' in response.content) + self.assertFalse(' xml:lang=""' in response.content) + + +class DateHierarchyTests(TestCase): + fixtures = ['admin-views-users.xml'] + + def setUp(self): + self.client.login(username='super', password='secret') + self.old_USE_THOUSAND_SEPARATOR = settings.USE_THOUSAND_SEPARATOR + self.old_USE_L10N = settings.USE_L10N + settings.USE_THOUSAND_SEPARATOR = True + settings.USE_L10N = True + + def tearDown(self): + settings.USE_THOUSAND_SEPARATOR = self.old_USE_THOUSAND_SEPARATOR + settings.USE_L10N = self.old_USE_L10N + formats.reset_format_cache() + + def assert_non_localized_year(self, response, year): + """Ensure that the year is not localized with + USE_THOUSAND_SEPARATOR. Refs #15234. + """ + self.assertNotContains(response, formats.number_format(year)) + + def assert_contains_year_link(self, response, date): + self.assertContains(response, '?release_date__year=%d"' % (date.year,)) + + def assert_contains_month_link(self, response, date): + self.assertContains( + response, '?release_date__year=%d&release_date__month=%d"' % ( + date.year, date.month)) + + def assert_contains_day_link(self, response, date): + self.assertContains( + response, '?release_date__year=%d&' + 'release_date__month=%d&release_date__day=%d"' % ( + date.year, date.month, date.day)) + + def test_empty(self): + """ + Ensure that no date hierarchy links display with empty changelist. + """ + response = self.client.get( + reverse('admin:admin_views_podcast_changelist')) + self.assertNotContains(response, 'release_date__year=') + self.assertNotContains(response, 'release_date__month=') + self.assertNotContains(response, 'release_date__day=') + + def test_single(self): + """ + Ensure that single day-level date hierarchy appears for single object. + """ + DATE = datetime.date(2000, 6, 30) + Podcast.objects.create(release_date=DATE) + url = reverse('admin:admin_views_podcast_changelist') + response = self.client.get(url) + self.assert_non_localized_year(response, 2000) + + def test_within_month(self): + """ + Ensure that day-level links appear for changelist within single month. + """ + DATES = (datetime.date(2000, 6, 30), + datetime.date(2000, 6, 15), + datetime.date(2000, 6, 3)) + for date in DATES: + Podcast.objects.create(release_date=date) + url = reverse('admin:admin_views_podcast_changelist') + response = self.client.get(url) + self.assert_non_localized_year(response, 2000) + + def test_within_year(self): + """ + Ensure that month-level links appear for changelist within single year. + """ + DATES = (datetime.date(2000, 1, 30), + datetime.date(2000, 3, 15), + datetime.date(2000, 5, 3)) + for date in DATES: + Podcast.objects.create(release_date=date) + url = reverse('admin:admin_views_podcast_changelist') + response = self.client.get(url) + # no day-level links + self.assertNotContains(response, 'release_date__day=') + self.assert_non_localized_year(response, 2000) + + def test_multiple_years(self): + """ + Ensure that year-level links appear for year-spanning changelist. + """ + DATES = (datetime.date(2001, 1, 30), + datetime.date(2003, 3, 15), + datetime.date(2005, 5, 3)) + for date in DATES: + Podcast.objects.create(release_date=date) + response = self.client.get( + reverse('admin:admin_views_podcast_changelist')) + # no day/month-level links + self.assertNotContains(response, 'release_date__day=') + self.assertNotContains(response, 'release_date__month=') + + # and make sure GET parameters still behave correctly + for date in DATES: + url = '%s?release_date__year=%d' % ( + reverse('admin:admin_views_podcast_changelist'), + date.year) + response = self.client.get(url) + self.assert_non_localized_year(response, 2000) + self.assert_non_localized_year(response, 2003) + self.assert_non_localized_year(response, 2005) + + response = self.client.get(url) + self.assert_non_localized_year(response, 2000) + self.assert_non_localized_year(response, 2003) + self.assert_non_localized_year(response, 2005) diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py index 59d625ba1296..450c3e6f9e0a 100644 --- a/tests/regressiontests/admin_widgets/models.py +++ b/tests/regressiontests/admin_widgets/models.py @@ -1,11 +1,8 @@ - -from django.conf import settings from django.db import models -from django.core.files.storage import default_storage from django.contrib.auth.models import User -class MyFileField(models.FileField): - pass +class MyFileField(models.FileField): + pass class Member(models.Model): name = models.CharField(max_length=100) @@ -69,99 +66,3 @@ class CarTire(models.Model): A single car tire. This to test that a user can only select their own cars. """ car = models.ForeignKey(Car) - -__test__ = {'WIDGETS_TESTS': """ ->>> from datetime import datetime ->>> from django.utils.html import escape, conditional_escape ->>> from django.core.files.uploadedfile import SimpleUploadedFile ->>> from django.contrib.admin.widgets import FilteredSelectMultiple, AdminSplitDateTime ->>> from django.contrib.admin.widgets import AdminFileWidget, ForeignKeyRawIdWidget, ManyToManyRawIdWidget ->>> from django.contrib.admin.widgets import RelatedFieldWidgetWrapper ->>> from django.utils.translation import activate, deactivate ->>> from django.conf import settings - - -Calling conditional_escape on the output of widget.render will simulate what -happens in the template. This is easier than setting up a template and context -for each test. - -Make sure that the Admin widgets render properly, that is, without their extra -HTML escaped. - ->>> w = FilteredSelectMultiple('test', False) ->>> print conditional_escape(w.render('test', 'test')) - - - ->>> w = FilteredSelectMultiple('test', True) ->>> print conditional_escape(w.render('test', 'test')) - - - ->>> w = AdminSplitDateTime() ->>> print conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30))) -

        Date:
        Time:

        ->>> activate('de-at') ->>> settings.USE_L10N = True ->>> w.is_localized = True ->>> print conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30))) -

        Datum:
        Zeit:

        ->>> deactivate() ->>> settings.USE_L10N = False - ->>> band = Band.objects.create(pk=1, name='Linkin Park') ->>> album = band.album_set.create(name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg') - ->>> w = AdminFileWidget() ->>> print conditional_escape(w.render('test', album.cover_art)) -Currently: albums\hybrid_theory.jpg
        Change: ->>> print conditional_escape(w.render('test', SimpleUploadedFile('test', 'content'))) - - ->>> rel = Album._meta.get_field('band').rel ->>> w = ForeignKeyRawIdWidget(rel) ->>> print conditional_escape(w.render('test', band.pk, attrs={})) - Lookup Linkin Park - ->>> m1 = Member.objects.create(pk=1, name='Chester') ->>> m2 = Member.objects.create(pk=2, name='Mike') ->>> band.members.add(m1, m2) - ->>> rel = Band._meta.get_field('members').rel ->>> w = ManyToManyRawIdWidget(rel) ->>> print conditional_escape(w.render('test', [m1.pk, m2.pk], attrs={})) - Lookup ->>> w._has_changed(None, None) -False ->>> w._has_changed([], None) -False ->>> w._has_changed(None, [u'1']) -True ->>> w._has_changed([1, 2], [u'1', u'2']) -False ->>> w._has_changed([1, 2], [u'1']) -True ->>> w._has_changed([1, 2], [u'1', u'3']) -True - -# Check that ForeignKeyRawIdWidget works with fields which aren't related to -# the model's primary key. ->>> apple = Inventory.objects.create(barcode=86, name='Apple') ->>> pear = Inventory.objects.create(barcode=22, name='Pear') ->>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple) ->>> rel = Inventory._meta.get_field('parent').rel ->>> w = ForeignKeyRawIdWidget(rel) ->>> print w.render('test', core.parent_id, attrs={}) - Lookup Apple - -# see #9258 ->>> hidden = Inventory.objects.create(barcode=93, name='Hidden', hidden=True) ->>> child_of_hidden = Inventory.objects.create(barcode=94, name='Child of hidden', parent=hidden) ->>> print w.render('test', child_of_hidden.parent_id, attrs={}) - Lookup Hidden -""" % { - 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, - 'STORAGE_URL': default_storage.url(''), -}} diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py index fd0c25ca1aac..cf3f965c1408 100644 --- a/tests/regressiontests/admin_widgets/tests.py +++ b/tests/regressiontests/admin_widgets/tests.py @@ -1,11 +1,25 @@ +# encoding: utf-8 + +from datetime import datetime +from unittest import TestCase + from django import forms +from django.conf import settings from django.contrib import admin from django.contrib.admin import widgets -from unittest import TestCase -from django.test import TestCase as DjangoTestCase +from django.contrib.admin.widgets import FilteredSelectMultiple, AdminSplitDateTime +from django.contrib.admin.widgets import (AdminFileWidget, ForeignKeyRawIdWidget, + ManyToManyRawIdWidget) +from django.core.files.storage import default_storage +from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import DateField +from django.test import TestCase as DjangoTestCase +from django.utils.html import conditional_escape +from django.utils.translation import activate, deactivate + import models + class AdminFormfieldForDBFieldTests(TestCase): """ Tests for correct behavior of ModelAdmin.formfield_for_dbfield @@ -102,6 +116,7 @@ def testChoicesWithRadioFields(self): def testInheritance(self): self.assertFormfield(models.Album, 'backside_art', widgets.AdminFileWidget) + class AdminFormfieldForDBFieldWithRequestTests(DjangoTestCase): fixtures = ["admin-widgets-users.xml"] @@ -114,6 +129,7 @@ def testFilterChoicesByRequestUser(self): self.assert_("BMW M3" not in response.content) self.assert_("Volkswagon Passat" in response.content) + class AdminForeignKeyWidgetChangeList(DjangoTestCase): fixtures = ["admin-widgets-users.xml"] admin_root = '/widget_admin' @@ -126,7 +142,8 @@ def tearDown(self): def test_changelist_foreignkey(self): response = self.client.get('%s/admin_widgets/car/' % self.admin_root) - self.failUnless('%s/auth/user/add/' % self.admin_root in response.content) + self.assertTrue('%s/auth/user/add/' % self.admin_root in response.content) + class AdminForeignKeyRawIdWidget(DjangoTestCase): fixtures = ["admin-widgets-users.xml"] @@ -151,3 +168,167 @@ def test_nonexistent_target_id(self): post_data) self.assertContains(response, 'Select a valid choice. That choice is not one of the available choices.') + + def test_invalid_target_id(self): + + for test_str in ('Iñtërnâtiônàlizætiøn', "1234'", -1234): + # This should result in an error message, not a server exception. + response = self.client.post('%s/admin_widgets/event/add/' % self.admin_root, + {"band": test_str}) + + self.assertContains(response, + 'Select a valid choice. That choice is not one of the available choices.') + + +class FilteredSelectMultipleWidgetTest(TestCase): + def test_render(self): + w = FilteredSelectMultiple('test', False) + self.assertEqual( + conditional_escape(w.render('test', 'test')), + '\n' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX} + ) + + def test_stacked_render(self): + w = FilteredSelectMultiple('test', True) + self.assertEqual( + conditional_escape(w.render('test', 'test')), + '\n' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX} + ) + + +class AdminSplitDateTimeWidgetTest(TestCase): + def test_render(self): + w = AdminSplitDateTime() + self.assertEqual( + conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30))), + '

        Date:
        Time:

        ', + ) + + def test_localization(self): + w = AdminSplitDateTime() + + activate('de-at') + old_USE_L10N = settings.USE_L10N + try: + settings.USE_L10N = True + w.is_localized = True + self.assertEqual( + conditional_escape(w.render('test', datetime(2007, 12, 1, 9, 30))), + '

        Datum:
        Zeit:

        ', + ) + finally: + deactivate() + settings.USE_L10N = old_USE_L10N + + +class AdminFileWidgetTest(DjangoTestCase): + def test_render(self): + band = models.Band.objects.create(name='Linkin Park') + album = band.album_set.create( + name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' + ) + + w = AdminFileWidget() + self.assertEqual( + conditional_escape(w.render('test', album.cover_art)), + 'Currently: albums\hybrid_theory.jpg
        Change: ' % {'STORAGE_URL': default_storage.url('')}, + ) + + self.assertEqual( + conditional_escape(w.render('test', SimpleUploadedFile('test', 'content'))), + '', + ) + + def test_render_escapes_html(self): + class StrangeFieldFile(object): + url = "something?chapter=1§=2©=3&lang=en" + + def __unicode__(self): + return u'''something
        .jpg''' + + widget = AdminFileWidget() + field = StrangeFieldFile() + output = widget.render('myfile', field) + self.assertFalse(field.url in output) + self.assertTrue(u'href="something?chapter=1&sect=2&copy=3&lang=en"' in output) + self.assertFalse(unicode(field) in output) + self.assertTrue(u'something<div onclick="alert('oops')">.jpg' in output) + + + +class ForeignKeyRawIdWidgetTest(DjangoTestCase): + def test_render(self): + band = models.Band.objects.create(name='Linkin Park') + band.album_set.create( + name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' + ) + rel = models.Album._meta.get_field('band').rel + + w = ForeignKeyRawIdWidget(rel) + self.assertEqual( + conditional_escape(w.render('test', band.pk, attrs={})), + ' Lookup Linkin Park' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX, "bandpk": band.pk}, + ) + + def test_relations_to_non_primary_key(self): + # Check that ForeignKeyRawIdWidget works with fields which aren't + # related to the model's primary key. + apple = models.Inventory.objects.create(barcode=86, name='Apple') + models.Inventory.objects.create(barcode=22, name='Pear') + core = models.Inventory.objects.create( + barcode=87, name='Core', parent=apple + ) + rel = models.Inventory._meta.get_field('parent').rel + w = ForeignKeyRawIdWidget(rel) + self.assertEqual( + w.render('test', core.parent_id, attrs={}), + ' Lookup Apple' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX}, + ) + + + def test_proper_manager_for_label_lookup(self): + # see #9258 + rel = models.Inventory._meta.get_field('parent').rel + w = ForeignKeyRawIdWidget(rel) + + hidden = models.Inventory.objects.create( + barcode=93, name='Hidden', hidden=True + ) + child_of_hidden = models.Inventory.objects.create( + barcode=94, name='Child of hidden', parent=hidden + ) + self.assertEqual( + w.render('test', child_of_hidden.parent_id, attrs={}), + ' Lookup Hidden' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX}, + ) + + +class ManyToManyRawIdWidgetTest(DjangoTestCase): + def test_render(self): + band = models.Band.objects.create(name='Linkin Park') + band.album_set.create( + name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg' + ) + + m1 = models.Member.objects.create(name='Chester') + m2 = models.Member.objects.create(name='Mike') + band.members.add(m1, m2) + rel = models.Band._meta.get_field('members').rel + + w = ManyToManyRawIdWidget(rel) + self.assertEqual( + conditional_escape(w.render('test', [m1.pk, m2.pk], attrs={})), + ' Lookup' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX, "m1pk": m1.pk, "m2pk": m2.pk}, + ) + + self.assertEqual( + conditional_escape(w.render('test', [m1.pk])), + ' Lookup' % {"ADMIN_MEDIA_PREFIX": settings.ADMIN_MEDIA_PREFIX, "m1pk": m1.pk}, + ) + + self.assertEqual(w._has_changed(None, None), False) + self.assertEqual(w._has_changed([], None), False) + self.assertEqual(w._has_changed(None, [u'1']), True) + self.assertEqual(w._has_changed([1, 2], [u'1', u'2']), False) + self.assertEqual(w._has_changed([1, 2], [u'1']), True) + self.assertEqual(w._has_changed([1, 2], [u'1', u'3']), True) diff --git a/tests/regressiontests/aggregation_regress/models.py b/tests/regressiontests/aggregation_regress/models.py index ba743575346c..ccef9a5fc89d 100644 --- a/tests/regressiontests/aggregation_regress/models.py +++ b/tests/regressiontests/aggregation_regress/models.py @@ -1,8 +1,6 @@ # coding: utf-8 -import pickle +from django.db import models -from django.db import connection, models, DEFAULT_DB_ALIAS -from django.conf import settings class Author(models.Model): name = models.CharField(max_length=100) @@ -12,6 +10,7 @@ class Author(models.Model): def __unicode__(self): return self.name + class Publisher(models.Model): name = models.CharField(max_length=255) num_awards = models.IntegerField() @@ -19,6 +18,7 @@ class Publisher(models.Model): def __unicode__(self): return self.name + class Book(models.Model): isbn = models.CharField(max_length=9) name = models.CharField(max_length=255) @@ -36,6 +36,7 @@ class Meta: def __unicode__(self): return self.name + class Store(models.Model): name = models.CharField(max_length=255) books = models.ManyToManyField(Book) @@ -50,329 +51,15 @@ class Entries(models.Model): Entry = models.CharField(unique=True, max_length=50) Exclude = models.BooleanField() + class Clues(models.Model): ID = models.AutoField(primary_key=True) EntryID = models.ForeignKey(Entries, verbose_name='Entry', db_column = 'Entry ID') Clue = models.CharField(max_length=150) + class HardbackBook(Book): weight = models.FloatField() def __unicode__(self): return "%s (hardback): %s" % (self.name, self.weight) - -__test__ = {'API_TESTS': """ ->>> from django.core import management ->>> from django.db.models import get_app, F - -# Reset the database representation of this app. -# This will return the database to a clean initial state. ->>> management.call_command('flush', verbosity=0, interactive=False) - ->>> from django.db.models import Avg, Sum, Count, Max, Min, StdDev, Variance - -# Ordering requests are ignored ->>> Author.objects.all().order_by('name').aggregate(Avg('age')) -{'age__avg': 37.4...} - -# Implicit ordering is also ignored ->>> Book.objects.all().aggregate(Sum('pages')) -{'pages__sum': 3703} - -# Baseline results ->>> Book.objects.all().aggregate(Sum('pages'), Avg('pages')) -{'pages__sum': 3703, 'pages__avg': 617.1...} - -# Empty values query doesn't affect grouping or results ->>> Book.objects.all().values().aggregate(Sum('pages'), Avg('pages')) -{'pages__sum': 3703, 'pages__avg': 617.1...} - -# Aggregate overrides extra selected column ->>> Book.objects.all().extra(select={'price_per_page' : 'price / pages'}).aggregate(Sum('pages')) -{'pages__sum': 3703} - -# Annotations get combined with extra select clauses ->>> sorted((k,v) for k,v in Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).get(pk=2).__dict__.items() if k != '_state') -[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)] - -# Order of the annotate/extra in the query doesn't matter ->>> sorted((k,v) for k,v in Book.objects.all().extra(select={'manufacture_cost' : 'price * .5'}).annotate(mean_auth_age=Avg('authors__age')).get(pk=2).__dict__.items()if k != '_state') -[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)] - -# Values queries can be combined with annotate and extra ->>> sorted((k,v) for k,v in Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).values().get(pk=2).items()if k != '_state') -[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)] - -# The order of the (empty) values, annotate and extra clauses doesn't matter ->>> sorted((k,v) for k,v in Book.objects.all().values().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).get(pk=2).items()if k != '_state') -[('contact_id', 3), ('id', 2), ('isbn', u'067232959'), ('manufacture_cost', ...11.545...), ('mean_auth_age', 45.0), ('name', u'Sams Teach Yourself Django in 24 Hours'), ('pages', 528), ('price', Decimal("23.09")), ('pubdate', datetime.date(2008, 3, 3)), ('publisher_id', 2), ('rating', 3.0)] - -# If the annotation precedes the values clause, it won't be included -# unless it is explicitly named ->>> sorted(Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name').get(pk=1).items()) -[('name', u'The Definitive Guide to Django: Web Development Done Right')] - ->>> sorted(Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name','mean_auth_age').get(pk=1).items()) -[('mean_auth_age', 34.5), ('name', u'The Definitive Guide to Django: Web Development Done Right')] - -# If an annotation isn't included in the values, it can still be used in a filter ->>> Book.objects.annotate(n_authors=Count('authors')).values('name').filter(n_authors__gt=2) -[{'name': u'Python Web Development with Django'}] - -# The annotations are added to values output if values() precedes annotate() ->>> sorted(Book.objects.all().values('name').annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).get(pk=1).items()) -[('mean_auth_age', 34.5), ('name', u'The Definitive Guide to Django: Web Development Done Right')] - -# Check that all of the objects are getting counted (allow_nulls) and that values respects the amount of objects ->>> len(Author.objects.all().annotate(Avg('friends__age')).values()) -9 - -# Check that consecutive calls to annotate accumulate in the query ->>> Book.objects.values('price').annotate(oldest=Max('authors__age')).order_by('oldest', 'price').annotate(Max('publisher__num_awards')) -[{'price': Decimal("30..."), 'oldest': 35, 'publisher__num_awards__max': 3}, {'price': Decimal("29.69"), 'oldest': 37, 'publisher__num_awards__max': 7}, {'price': Decimal("23.09"), 'oldest': 45, 'publisher__num_awards__max': 1}, {'price': Decimal("75..."), 'oldest': 57, 'publisher__num_awards__max': 9}, {'price': Decimal("82.8..."), 'oldest': 57, 'publisher__num_awards__max': 7}] - -# Aggregates can be composed over annotations. -# The return type is derived from the composed aggregate ->>> Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('pages'), Max('price'), Sum('num_authors'), Avg('num_authors')) -{'num_authors__sum': 10, 'num_authors__avg': 1.66..., 'pages__max': 1132, 'price__max': Decimal("82.80")} - -# Bad field requests in aggregates are caught and reported ->>> Book.objects.all().aggregate(num_authors=Count('foo')) -Traceback (most recent call last): -... -FieldError: Cannot resolve keyword 'foo' into field. Choices are: authors, contact, hardbackbook, id, isbn, name, pages, price, pubdate, publisher, rating, store - ->>> Book.objects.all().annotate(num_authors=Count('foo')) -Traceback (most recent call last): -... -FieldError: Cannot resolve keyword 'foo' into field. Choices are: authors, contact, hardbackbook, id, isbn, name, pages, price, pubdate, publisher, rating, store - ->>> Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('foo')) -Traceback (most recent call last): -... -FieldError: Cannot resolve keyword 'foo' into field. Choices are: authors, contact, hardbackbook, id, isbn, name, pages, price, pubdate, publisher, rating, store, num_authors - -# Old-style count aggregations can be mixed with new-style ->>> Book.objects.annotate(num_authors=Count('authors')).count() -6 - -# Non-ordinal, non-computed Aggregates over annotations correctly inherit -# the annotation's internal type if the annotation is ordinal or computed ->>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Max('num_authors')) -{'num_authors__max': 3} - ->>> Publisher.objects.annotate(avg_price=Avg('book__price')).aggregate(Max('avg_price')) -{'avg_price__max': 75.0...} - -# Aliases are quoted to protected aliases that might be reserved names ->>> Book.objects.aggregate(number=Max('pages'), select=Max('pages')) -{'number': 1132, 'select': 1132} - -# Regression for #10064: select_related() plays nice with aggregates ->>> sorted(Book.objects.select_related('publisher').annotate(num_authors=Count('authors')).values()[0].iteritems()) -[('contact_id', 8), ('id', 5), ('isbn', u'013790395'), ('name', u'Artificial Intelligence: A Modern Approach'), ('num_authors', 2), ('pages', 1132), ('price', Decimal("82.8...")), ('pubdate', datetime.date(1995, 1, 15)), ('publisher_id', 3), ('rating', 4.0)] - -# Regression for #10010: exclude on an aggregate field is correctly negated ->>> len(Book.objects.annotate(num_authors=Count('authors'))) -6 ->>> len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=2)) -1 ->>> len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__gt=2)) -5 - ->>> len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__lt=3).exclude(num_authors__lt=2)) -2 ->>> len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__lt=2).filter(num_authors__lt=3)) -2 - -# Aggregates can be used with F() expressions -# ... where the F() is pushed into the HAVING clause ->>> Publisher.objects.annotate(num_books=Count('book')).filter(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards') -[{'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}, {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}] - ->>> Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards') -[{'num_books': 2, 'name': u'Apress', 'num_awards': 3}, {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}, {'num_books': 1, 'name': u'Sams', 'num_awards': 1}] - -# ... and where the F() references an aggregate ->>> Publisher.objects.annotate(num_books=Count('book')).filter(num_awards__gt=2*F('num_books')).order_by('name').values('name','num_books','num_awards') -[{'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}, {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7}] - ->>> Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards') -[{'num_books': 2, 'name': u'Apress', 'num_awards': 3}, {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}, {'num_books': 1, 'name': u'Sams', 'num_awards': 1}] - -# Tests on fields with non-default table and column names. ->>> Clues.objects.values('EntryID__Entry').annotate(Appearances=Count('EntryID'), Distinct_Clues=Count('Clue', distinct=True)) -[] - ->>> Entries.objects.annotate(clue_count=Count('clues__ID')) -[] - -# Regression for #10089: Check handling of empty result sets with aggregates ->>> Book.objects.filter(id__in=[]).count() -0 - ->>> Book.objects.filter(id__in=[]).aggregate(num_authors=Count('authors'), avg_authors=Avg('authors'), max_authors=Max('authors'), max_price=Max('price'), max_rating=Max('rating')) -{'max_authors': None, 'max_rating': None, 'num_authors': 0, 'avg_authors': None, 'max_price': None} - ->>> list(Publisher.objects.filter(pk=5).annotate(num_authors=Count('book__authors'), avg_authors=Avg('book__authors'), max_authors=Max('book__authors'), max_price=Max('book__price'), max_rating=Max('book__rating')).values()) == [{'max_authors': None, 'name': u"Jonno's House of Books", 'num_awards': 0, 'max_price': None, 'num_authors': 0, 'max_rating': None, 'id': 5, 'avg_authors': None}] -True - -# Regression for #10113 - Fields mentioned in order_by() must be included in the GROUP BY. -# This only becomes a problem when the order_by introduces a new join. ->>> Book.objects.annotate(num_authors=Count('authors')).order_by('publisher__name', 'name') -[, , , , , ] - -# Regression for #10127 - Empty select_related() works with annotate ->>> books = Book.objects.all().filter(rating__lt=4.5).select_related().annotate(Avg('authors__age')) ->>> sorted([(b.name, b.authors__age__avg, b.publisher.name, b.contact.name) for b in books]) -[(u'Artificial Intelligence: A Modern Approach', 51.5, u'Prentice Hall', u'Peter Norvig'), (u'Practical Django Projects', 29.0, u'Apress', u'James Bennett'), (u'Python Web Development with Django', 30.3..., u'Prentice Hall', u'Jeffrey Forcier'), (u'Sams Teach Yourself Django in 24 Hours', 45.0, u'Sams', u'Brad Dayley')] - -# Regression for #10132 - If the values() clause only mentioned extra(select=) columns, those columns are used for grouping ->>> Book.objects.extra(select={'pub':'publisher_id'}).values('pub').annotate(Count('id')).order_by('pub') -[{'pub': 1, 'id__count': 2}, {'pub': 2, 'id__count': 1}, {'pub': 3, 'id__count': 2}, {'pub': 4, 'id__count': 1}] - ->>> Book.objects.extra(select={'pub':'publisher_id','foo':'pages'}).values('pub').annotate(Count('id')).order_by('pub') -[{'pub': 1, 'id__count': 2}, {'pub': 2, 'id__count': 1}, {'pub': 3, 'id__count': 2}, {'pub': 4, 'id__count': 1}] - -# Regression for #10182 - Queries with aggregate calls are correctly realiased when used in a subquery ->>> ids = Book.objects.filter(pages__gt=100).annotate(n_authors=Count('authors')).filter(n_authors__gt=2).order_by('n_authors') ->>> Book.objects.filter(id__in=ids) -[] - -# Regression for #10197 -- Queries with aggregates can be pickled. -# First check that pickling is possible at all. No crash = success ->>> qs = Book.objects.annotate(num_authors=Count('authors')) ->>> out = pickle.dumps(qs) - -# Then check that the round trip works. ->>> query = qs.query.get_compiler(qs.db).as_sql()[0] ->>> select_fields = qs.query.select_fields ->>> query2 = pickle.loads(pickle.dumps(qs)) ->>> query2.query.get_compiler(query2.db).as_sql()[0] == query -True ->>> query2.query.select_fields = select_fields - -# Regression for #10199 - Aggregate calls clone the original query so the original query can still be used ->>> books = Book.objects.all() ->>> _ = books.aggregate(Avg('authors__age')) ->>> books.all() -[, , , , , ] - -# Regression for #10248 - Annotations work with DateQuerySets ->>> Book.objects.annotate(num_authors=Count('authors')).filter(num_authors=2).dates('pubdate', 'day') -[datetime.datetime(1995, 1, 15, 0, 0), datetime.datetime(2007, 12, 6, 0, 0)] - -# Regression for #10290 - extra selects with parameters can be used for -# grouping. ->>> qs = Book.objects.all().annotate(mean_auth_age=Avg('authors__age')).extra(select={'sheets' : '(pages + %s) / %s'}, select_params=[1, 2]).order_by('sheets').values('sheets') ->>> [int(x['sheets']) for x in qs] -[150, 175, 224, 264, 473, 566] - -# Regression for 10425 - annotations don't get in the way of a count() clause ->>> Book.objects.values('publisher').annotate(Count('publisher')).count() -4 - ->>> Book.objects.annotate(Count('publisher')).values('publisher').count() -6 - ->>> publishers = Publisher.objects.filter(id__in=(1,2)) ->>> publishers -[, ] - ->>> publishers = publishers.annotate(n_books=models.Count('book')) ->>> publishers[0].n_books -2 - ->>> publishers -[, ] - ->>> books = Book.objects.filter(publisher__in=publishers) ->>> books -[, , ] - ->>> publishers -[, ] - - -# Regression for 10666 - inherited fields work with annotations and aggregations ->>> HardbackBook.objects.aggregate(n_pages=Sum('book_ptr__pages')) -{'n_pages': 2078} - ->>> HardbackBook.objects.aggregate(n_pages=Sum('pages')) -{'n_pages': 2078} - ->>> HardbackBook.objects.annotate(n_authors=Count('book_ptr__authors')).values('name','n_authors') -[{'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'}, {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'}] - ->>> HardbackBook.objects.annotate(n_authors=Count('authors')).values('name','n_authors') -[{'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'}, {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'}] - -# Regression for #10766 - Shouldn't be able to reference an aggregate fields in an an aggregate() call. ->>> Book.objects.all().annotate(mean_age=Avg('authors__age')).annotate(Avg('mean_age')) -Traceback (most recent call last): -... -FieldError: Cannot compute Avg('mean_age'): 'mean_age' is an aggregate - -""" -} - -def run_stddev_tests(): - """Check to see if StdDev/Variance tests should be run. - - Stddev and Variance are not guaranteed to be available for SQLite, and - are not available for PostgreSQL before 8.2. - """ - if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.sqlite3': - return False - - class StdDevPop(object): - sql_function = 'STDDEV_POP' - - try: - connection.ops.check_aggregate_support(StdDevPop()) - except: - return False - return True - -if run_stddev_tests(): - __test__['API_TESTS'] += """ ->>> Book.objects.aggregate(StdDev('pages')) -{'pages__stddev': 311.46...} - ->>> Book.objects.aggregate(StdDev('rating')) -{'rating__stddev': 0.60...} - ->>> Book.objects.aggregate(StdDev('price')) -{'price__stddev': 24.16...} - - ->>> Book.objects.aggregate(StdDev('pages', sample=True)) -{'pages__stddev': 341.19...} - ->>> Book.objects.aggregate(StdDev('rating', sample=True)) -{'rating__stddev': 0.66...} - ->>> Book.objects.aggregate(StdDev('price', sample=True)) -{'price__stddev': 26.46...} - - ->>> Book.objects.aggregate(Variance('pages')) -{'pages__variance': 97010.80...} - ->>> Book.objects.aggregate(Variance('rating')) -{'rating__variance': 0.36...} - ->>> Book.objects.aggregate(Variance('price')) -{'price__variance': 583.77...} - - ->>> Book.objects.aggregate(Variance('pages', sample=True)) -{'pages__variance': 116412.96...} - ->>> Book.objects.aggregate(Variance('rating', sample=True)) -{'rating__variance': 0.44...} - ->>> Book.objects.aggregate(Variance('price', sample=True)) -{'price__variance': 700.53...} - -""" diff --git a/tests/regressiontests/aggregation_regress/tests.py b/tests/regressiontests/aggregation_regress/tests.py index 3c4bdfa47dbf..3e6393e6955e 100644 --- a/tests/regressiontests/aggregation_regress/tests.py +++ b/tests/regressiontests/aggregation_regress/tests.py @@ -1,12 +1,40 @@ +import datetime +import pickle +from decimal import Decimal +from operator import attrgetter + from django.conf import settings -from django.test import TestCase +from django.core.exceptions import FieldError from django.db import DEFAULT_DB_ALIAS -from django.db.models import Count, Max +from django.db.models import Count, Max, Avg, Sum, StdDev, Variance, F, Q +from django.test import TestCase, Approximate + +from models import Author, Book, Publisher, Clues, Entries, HardbackBook + + +def run_stddev_tests(): + """Check to see if StdDev/Variance tests should be run. + + Stddev and Variance are not guaranteed to be available for SQLite, and + are not available for PostgreSQL before 8.2. + """ + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.sqlite3': + return False + + class StdDevPop(object): + sql_function = 'STDDEV_POP' -from regressiontests.aggregation_regress.models import * + try: + connection.ops.check_aggregate_support(StdDevPop()) + except: + return False + return True class AggregationTests(TestCase): + def assertObjectAttrs(self, obj, **kwargs): + for attr, value in kwargs.iteritems(): + self.assertEqual(getattr(obj, attr), value) def test_aggregates_in_where_clause(self): """ @@ -70,3 +98,747 @@ def test_annotate_with_extra(self): }).annotate(total_books=Count('book')) # force execution of the query list(qs) + + def test_aggregate(self): + # Ordering requests are ignored + self.assertEqual( + Author.objects.order_by("name").aggregate(Avg("age")), + {"age__avg": Approximate(37.444, places=1)} + ) + + # Implicit ordering is also ignored + self.assertEqual( + Book.objects.aggregate(Sum("pages")), + {"pages__sum": 3703}, + ) + + # Baseline results + self.assertEqual( + Book.objects.aggregate(Sum('pages'), Avg('pages')), + {'pages__sum': 3703, 'pages__avg': Approximate(617.166, places=2)} + ) + + # Empty values query doesn't affect grouping or results + self.assertEqual( + Book.objects.values().aggregate(Sum('pages'), Avg('pages')), + {'pages__sum': 3703, 'pages__avg': Approximate(617.166, places=2)} + ) + + # Aggregate overrides extra selected column + self.assertEqual( + Book.objects.extra(select={'price_per_page' : 'price / pages'}).aggregate(Sum('pages')), + {'pages__sum': 3703} + ) + + def test_annotation(self): + # Annotations get combined with extra select clauses + obj = Book.objects.annotate(mean_auth_age=Avg("authors__age")).extra(select={"manufacture_cost": "price * .5"}).get(pk=2) + self.assertObjectAttrs(obj, + contact_id=3, + id=2, + isbn=u'067232959', + mean_auth_age=45.0, + name='Sams Teach Yourself Django in 24 Hours', + pages=528, + price=Decimal("23.09"), + pubdate=datetime.date(2008, 3, 3), + publisher_id=2, + rating=3.0 + ) + # Different DB backends return different types for the extra select computation + self.assertTrue(obj.manufacture_cost == 11.545 or obj.manufacture_cost == Decimal('11.545')) + + # Order of the annotate/extra in the query doesn't matter + obj = Book.objects.extra(select={'manufacture_cost' : 'price * .5'}).annotate(mean_auth_age=Avg('authors__age')).get(pk=2) + self.assertObjectAttrs(obj, + contact_id=3, + id=2, + isbn=u'067232959', + mean_auth_age=45.0, + name=u'Sams Teach Yourself Django in 24 Hours', + pages=528, + price=Decimal("23.09"), + pubdate=datetime.date(2008, 3, 3), + publisher_id=2, + rating=3.0 + ) + # Different DB backends return different types for the extra select computation + self.assertTrue(obj.manufacture_cost == 11.545 or obj.manufacture_cost == Decimal('11.545')) + + # Values queries can be combined with annotate and extra + obj = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).values().get(pk=2) + manufacture_cost = obj['manufacture_cost'] + self.assertTrue(manufacture_cost == 11.545 or manufacture_cost == Decimal('11.545')) + del obj['manufacture_cost'] + self.assertEqual(obj, { + "contact_id": 3, + "id": 2, + "isbn": u"067232959", + "mean_auth_age": 45.0, + "name": u"Sams Teach Yourself Django in 24 Hours", + "pages": 528, + "price": Decimal("23.09"), + "pubdate": datetime.date(2008, 3, 3), + "publisher_id": 2, + "rating": 3.0, + }) + + # The order of the (empty) values, annotate and extra clauses doesn't + # matter + obj = Book.objects.values().annotate(mean_auth_age=Avg('authors__age')).extra(select={'manufacture_cost' : 'price * .5'}).get(pk=2) + manufacture_cost = obj['manufacture_cost'] + self.assertTrue(manufacture_cost == 11.545 or manufacture_cost == Decimal('11.545')) + del obj['manufacture_cost'] + self.assertEqual(obj, { + 'contact_id': 3, + 'id': 2, + 'isbn': u'067232959', + 'mean_auth_age': 45.0, + 'name': u'Sams Teach Yourself Django in 24 Hours', + 'pages': 528, + 'price': Decimal("23.09"), + 'pubdate': datetime.date(2008, 3, 3), + 'publisher_id': 2, + 'rating': 3.0 + }) + + # If the annotation precedes the values clause, it won't be included + # unless it is explicitly named + obj = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name').get(pk=1) + self.assertEqual(obj, { + "name": u'The Definitive Guide to Django: Web Development Done Right', + }) + + obj = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).values('name','mean_auth_age').get(pk=1) + self.assertEqual(obj, { + 'mean_auth_age': 34.5, + 'name': u'The Definitive Guide to Django: Web Development Done Right', + }) + + # If an annotation isn't included in the values, it can still be used + # in a filter + qs = Book.objects.annotate(n_authors=Count('authors')).values('name').filter(n_authors__gt=2) + self.assertQuerysetEqual( + qs, [ + {"name": u'Python Web Development with Django'} + ], + lambda b: b, + ) + + # The annotations are added to values output if values() precedes + # annotate() + obj = Book.objects.values('name').annotate(mean_auth_age=Avg('authors__age')).extra(select={'price_per_page' : 'price / pages'}).get(pk=1) + self.assertEqual(obj, { + 'mean_auth_age': 34.5, + 'name': u'The Definitive Guide to Django: Web Development Done Right', + }) + + # Check that all of the objects are getting counted (allow_nulls) and + # that values respects the amount of objects + self.assertEqual( + len(Author.objects.annotate(Avg('friends__age')).values()), + 9 + ) + + # Check that consecutive calls to annotate accumulate in the query + qs = Book.objects.values('price').annotate(oldest=Max('authors__age')).order_by('oldest', 'price').annotate(Max('publisher__num_awards')) + self.assertQuerysetEqual( + qs, [ + {'price': Decimal("30"), 'oldest': 35, 'publisher__num_awards__max': 3}, + {'price': Decimal("29.69"), 'oldest': 37, 'publisher__num_awards__max': 7}, + {'price': Decimal("23.09"), 'oldest': 45, 'publisher__num_awards__max': 1}, + {'price': Decimal("75"), 'oldest': 57, 'publisher__num_awards__max': 9}, + {'price': Decimal("82.8"), 'oldest': 57, 'publisher__num_awards__max': 7} + ], + lambda b: b, + ) + + def test_aggrate_annotation(self): + # Aggregates can be composed over annotations. + # The return type is derived from the composed aggregate + vals = Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('pages'), Max('price'), Sum('num_authors'), Avg('num_authors')) + self.assertEqual(vals, { + 'num_authors__sum': 10, + 'num_authors__avg': Approximate(1.666, places=2), + 'pages__max': 1132, + 'price__max': Decimal("82.80") + }) + + def test_field_error(self): + # Bad field requests in aggregates are caught and reported + self.assertRaises( + FieldError, + lambda: Book.objects.all().aggregate(num_authors=Count('foo')) + ) + + self.assertRaises( + FieldError, + lambda: Book.objects.all().annotate(num_authors=Count('foo')) + ) + + self.assertRaises( + FieldError, + lambda: Book.objects.all().annotate(num_authors=Count('authors__id')).aggregate(Max('foo')) + ) + + def test_more(self): + # Old-style count aggregations can be mixed with new-style + self.assertEqual( + Book.objects.annotate(num_authors=Count('authors')).count(), + 6 + ) + + # Non-ordinal, non-computed Aggregates over annotations correctly + # inherit the annotation's internal type if the annotation is ordinal + # or computed + vals = Book.objects.annotate(num_authors=Count('authors')).aggregate(Max('num_authors')) + self.assertEqual( + vals, + {'num_authors__max': 3} + ) + + vals = Publisher.objects.annotate(avg_price=Avg('book__price')).aggregate(Max('avg_price')) + self.assertEqual( + vals, + {'avg_price__max': 75.0} + ) + + # Aliases are quoted to protected aliases that might be reserved names + vals = Book.objects.aggregate(number=Max('pages'), select=Max('pages')) + self.assertEqual( + vals, + {'number': 1132, 'select': 1132} + ) + + # Regression for #10064: select_related() plays nice with aggregates + obj = Book.objects.select_related('publisher').annotate(num_authors=Count('authors')).values()[0] + self.assertEqual(obj, { + 'contact_id': 8, + 'id': 5, + 'isbn': u'013790395', + 'name': u'Artificial Intelligence: A Modern Approach', + 'num_authors': 2, + 'pages': 1132, + 'price': Decimal("82.8"), + 'pubdate': datetime.date(1995, 1, 15), + 'publisher_id': 3, + 'rating': 4.0, + }) + + # Regression for #10010: exclude on an aggregate field is correctly + # negated + self.assertEqual( + len(Book.objects.annotate(num_authors=Count('authors'))), + 6 + ) + self.assertEqual( + len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=2)), + 1 + ) + self.assertEqual( + len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__gt=2)), + 5 + ) + + self.assertEqual( + len(Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__lt=3).exclude(num_authors__lt=2)), + 2 + ) + self.assertEqual( + len(Book.objects.annotate(num_authors=Count('authors')).exclude(num_authors__lt=2).filter(num_authors__lt=3)), + 2 + ) + + def test_aggregate_fexpr(self): + # Aggregates can be used with F() expressions + # ... where the F() is pushed into the HAVING clause + qs = Publisher.objects.annotate(num_books=Count('book')).filter(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards') + self.assertQuerysetEqual( + qs, [ + {'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}, + {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7} + ], + lambda p: p, + ) + + qs = Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards') + self.assertQuerysetEqual( + qs, [ + {'num_books': 2, 'name': u'Apress', 'num_awards': 3}, + {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}, + {'num_books': 1, 'name': u'Sams', 'num_awards': 1} + ], + lambda p: p, + ) + + # ... and where the F() references an aggregate + qs = Publisher.objects.annotate(num_books=Count('book')).filter(num_awards__gt=2*F('num_books')).order_by('name').values('name','num_books','num_awards') + self.assertQuerysetEqual( + qs, [ + {'num_books': 1, 'name': u'Morgan Kaufmann', 'num_awards': 9}, + {'num_books': 2, 'name': u'Prentice Hall', 'num_awards': 7} + ], + lambda p: p, + ) + + qs = Publisher.objects.annotate(num_books=Count('book')).exclude(num_books__lt=F('num_awards')/2).order_by('name').values('name','num_books','num_awards') + self.assertQuerysetEqual( + qs, [ + {'num_books': 2, 'name': u'Apress', 'num_awards': 3}, + {'num_books': 0, 'name': u"Jonno's House of Books", 'num_awards': 0}, + {'num_books': 1, 'name': u'Sams', 'num_awards': 1} + ], + lambda p: p, + ) + + def test_db_col_table(self): + # Tests on fields with non-default table and column names. + qs = Clues.objects.values('EntryID__Entry').annotate(Appearances=Count('EntryID'), Distinct_Clues=Count('Clue', distinct=True)) + self.assertQuerysetEqual(qs, []) + + qs = Entries.objects.annotate(clue_count=Count('clues__ID')) + self.assertQuerysetEqual(qs, []) + + def test_empty(self): + # Regression for #10089: Check handling of empty result sets with + # aggregates + self.assertEqual( + Book.objects.filter(id__in=[]).count(), + 0 + ) + + vals = Book.objects.filter(id__in=[]).aggregate(num_authors=Count('authors'), avg_authors=Avg('authors'), max_authors=Max('authors'), max_price=Max('price'), max_rating=Max('rating')) + self.assertEqual( + vals, + {'max_authors': None, 'max_rating': None, 'num_authors': 0, 'avg_authors': None, 'max_price': None} + ) + + qs = Publisher.objects.filter(pk=5).annotate(num_authors=Count('book__authors'), avg_authors=Avg('book__authors'), max_authors=Max('book__authors'), max_price=Max('book__price'), max_rating=Max('book__rating')).values() + self.assertQuerysetEqual( + qs, [ + {'max_authors': None, 'name': u"Jonno's House of Books", 'num_awards': 0, 'max_price': None, 'num_authors': 0, 'max_rating': None, 'id': 5, 'avg_authors': None} + ], + lambda p: p + ) + + def test_more_more(self): + # Regression for #10113 - Fields mentioned in order_by() must be + # included in the GROUP BY. This only becomes a problem when the + # order_by introduces a new join. + self.assertQuerysetEqual( + Book.objects.annotate(num_authors=Count('authors')).order_by('publisher__name', 'name'), [ + "Practical Django Projects", + "The Definitive Guide to Django: Web Development Done Right", + "Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp", + "Artificial Intelligence: A Modern Approach", + "Python Web Development with Django", + "Sams Teach Yourself Django in 24 Hours", + ], + lambda b: b.name + ) + + # Regression for #10127 - Empty select_related() works with annotate + qs = Book.objects.filter(rating__lt=4.5).select_related().annotate(Avg('authors__age')) + self.assertQuerysetEqual( + qs, [ + (u'Artificial Intelligence: A Modern Approach', 51.5, u'Prentice Hall', u'Peter Norvig'), + (u'Practical Django Projects', 29.0, u'Apress', u'James Bennett'), + (u'Python Web Development with Django', Approximate(30.333, places=2), u'Prentice Hall', u'Jeffrey Forcier'), + (u'Sams Teach Yourself Django in 24 Hours', 45.0, u'Sams', u'Brad Dayley') + ], + lambda b: (b.name, b.authors__age__avg, b.publisher.name, b.contact.name) + ) + + # Regression for #10132 - If the values() clause only mentioned extra + # (select=) columns, those columns are used for grouping + qs = Book.objects.extra(select={'pub':'publisher_id'}).values('pub').annotate(Count('id')).order_by('pub') + self.assertQuerysetEqual( + qs, [ + {'pub': 1, 'id__count': 2}, + {'pub': 2, 'id__count': 1}, + {'pub': 3, 'id__count': 2}, + {'pub': 4, 'id__count': 1} + ], + lambda b: b + ) + + qs = Book.objects.extra(select={'pub':'publisher_id', 'foo':'pages'}).values('pub').annotate(Count('id')).order_by('pub') + self.assertQuerysetEqual( + qs, [ + {'pub': 1, 'id__count': 2}, + {'pub': 2, 'id__count': 1}, + {'pub': 3, 'id__count': 2}, + {'pub': 4, 'id__count': 1} + ], + lambda b: b + ) + + # Regression for #10182 - Queries with aggregate calls are correctly + # realiased when used in a subquery + ids = Book.objects.filter(pages__gt=100).annotate(n_authors=Count('authors')).filter(n_authors__gt=2).order_by('n_authors') + self.assertQuerysetEqual( + Book.objects.filter(id__in=ids), [ + "Python Web Development with Django", + ], + lambda b: b.name + ) + + def test_duplicate_alias(self): + # Regression for #11256 - duplicating a default alias raises ValueError. + self.assertRaises(ValueError, Book.objects.all().annotate, Avg('authors__age'), authors__age__avg=Avg('authors__age')) + + def test_field_name_conflict(self): + # Regression for #11256 - providing an aggregate name that conflicts with a field name on the model raises ValueError + self.assertRaises(ValueError, Author.objects.annotate, age=Avg('friends__age')) + + def test_m2m_name_conflict(self): + # Regression for #11256 - providing an aggregate name that conflicts with an m2m name on the model raises ValueError + self.assertRaises(ValueError, Author.objects.annotate, friends=Count('friends')) + + def test_values_queryset_non_conflict(self): + # Regression for #14707 -- If you're using a values query set, some potential conflicts are avoided. + + # age is a field on Author, so it shouldn't be allowed as an aggregate. + # But age isn't included in the ValuesQuerySet, so it is. + results = Author.objects.values('name').annotate(age=Count('book_contact_set')).order_by('name') + self.assertEquals(len(results), 9) + self.assertEquals(results[0]['name'], u'Adrian Holovaty') + self.assertEquals(results[0]['age'], 1) + + # Same problem, but aggregating over m2m fields + results = Author.objects.values('name').annotate(age=Avg('friends__age')).order_by('name') + self.assertEquals(len(results), 9) + self.assertEquals(results[0]['name'], u'Adrian Holovaty') + self.assertEquals(results[0]['age'], 32.0) + + # Same problem, but colliding with an m2m field + results = Author.objects.values('name').annotate(friends=Count('friends')).order_by('name') + self.assertEquals(len(results), 9) + self.assertEquals(results[0]['name'], u'Adrian Holovaty') + self.assertEquals(results[0]['friends'], 2) + + def test_reverse_relation_name_conflict(self): + # Regression for #11256 - providing an aggregate name that conflicts with a reverse-related name on the model raises ValueError + self.assertRaises(ValueError, Author.objects.annotate, book_contact_set=Avg('friends__age')) + + def test_pickle(self): + # Regression for #10197 -- Queries with aggregates can be pickled. + # First check that pickling is possible at all. No crash = success + qs = Book.objects.annotate(num_authors=Count('authors')) + pickle.dumps(qs) + + # Then check that the round trip works. + query = qs.query.get_compiler(qs.db).as_sql()[0] + qs2 = pickle.loads(pickle.dumps(qs)) + self.assertEqual( + qs2.query.get_compiler(qs2.db).as_sql()[0], + query, + ) + + def test_more_more_more(self): + # Regression for #10199 - Aggregate calls clone the original query so + # the original query can still be used + books = Book.objects.all() + books.aggregate(Avg("authors__age")) + self.assertQuerysetEqual( + books.all(), [ + u'Artificial Intelligence: A Modern Approach', + u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp', + u'Practical Django Projects', + u'Python Web Development with Django', + u'Sams Teach Yourself Django in 24 Hours', + u'The Definitive Guide to Django: Web Development Done Right' + ], + lambda b: b.name + ) + + # Regression for #10248 - Annotations work with DateQuerySets + qs = Book.objects.annotate(num_authors=Count('authors')).filter(num_authors=2).dates('pubdate', 'day') + self.assertQuerysetEqual( + qs, [ + datetime.datetime(1995, 1, 15, 0, 0), + datetime.datetime(2007, 12, 6, 0, 0) + ], + lambda b: b + ) + + # Regression for #10290 - extra selects with parameters can be used for + # grouping. + qs = Book.objects.annotate(mean_auth_age=Avg('authors__age')).extra(select={'sheets' : '(pages + %s) / %s'}, select_params=[1, 2]).order_by('sheets').values('sheets') + self.assertQuerysetEqual( + qs, [ + 150, + 175, + 224, + 264, + 473, + 566 + ], + lambda b: int(b["sheets"]) + ) + + # Regression for 10425 - annotations don't get in the way of a count() + # clause + self.assertEqual( + Book.objects.values('publisher').annotate(Count('publisher')).count(), + 4 + ) + self.assertEqual( + Book.objects.annotate(Count('publisher')).values('publisher').count(), + 6 + ) + + publishers = Publisher.objects.filter(id__in=[1, 2]) + self.assertEqual( + sorted(p.name for p in publishers), + [ + "Apress", + "Sams" + ] + ) + + publishers = publishers.annotate(n_books=Count("book")) + self.assertEqual( + publishers[0].n_books, + 2 + ) + + self.assertEqual( + sorted(p.name for p in publishers), + [ + "Apress", + "Sams" + ] + ) + + books = Book.objects.filter(publisher__in=publishers) + self.assertQuerysetEqual( + books, [ + "Practical Django Projects", + "Sams Teach Yourself Django in 24 Hours", + "The Definitive Guide to Django: Web Development Done Right", + ], + lambda b: b.name + ) + self.assertEqual( + sorted(p.name for p in publishers), + [ + "Apress", + "Sams" + ] + ) + + # Regression for 10666 - inherited fields work with annotations and + # aggregations + self.assertEqual( + HardbackBook.objects.aggregate(n_pages=Sum('book_ptr__pages')), + {'n_pages': 2078} + ) + + self.assertEqual( + HardbackBook.objects.aggregate(n_pages=Sum('pages')), + {'n_pages': 2078}, + ) + + qs = HardbackBook.objects.annotate(n_authors=Count('book_ptr__authors')).values('name', 'n_authors') + self.assertQuerysetEqual( + qs, [ + {'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'}, + {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'} + ], + lambda h: h + ) + + qs = HardbackBook.objects.annotate(n_authors=Count('authors')).values('name', 'n_authors') + self.assertQuerysetEqual( + qs, [ + {'n_authors': 2, 'name': u'Artificial Intelligence: A Modern Approach'}, + {'n_authors': 1, 'name': u'Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp'} + ], + lambda h: h, + ) + + # Regression for #10766 - Shouldn't be able to reference an aggregate + # fields in an an aggregate() call. + self.assertRaises( + FieldError, + lambda: Book.objects.annotate(mean_age=Avg('authors__age')).annotate(Avg('mean_age')) + ) + + def test_empty_filter_count(self): + self.assertEqual( + Author.objects.filter(id__in=[]).annotate(Count("friends")).count(), + 0 + ) + + def test_empty_filter_aggregate(self): + self.assertEqual( + Author.objects.filter(id__in=[]).annotate(Count("friends")).aggregate(Count("pk")), + {"pk__count": None} + ) + + def test_annotate_and_join(self): + self.assertEqual( + Author.objects.annotate(c=Count("friends__name")).exclude(friends__name="Joe").count(), + Author.objects.count() + ) + + def test_f_expression_annotation(self): + # Books with less than 200 pages per author. + qs = Book.objects.values("name").annotate( + n_authors=Count("authors") + ).filter( + pages__lt=F("n_authors") * 200 + ).values_list("pk") + self.assertQuerysetEqual( + Book.objects.filter(pk__in=qs), [ + "Python Web Development with Django" + ], + attrgetter("name") + ) + + def test_values_annotate_values(self): + qs = Book.objects.values("name").annotate( + n_authors=Count("authors") + ).values_list("pk", flat=True) + self.assertEqual(list(qs), list(Book.objects.values_list("pk", flat=True))) + + def test_having_group_by(self): + # Test that when a field occurs on the LHS of a HAVING clause that it + # appears correctly in the GROUP BY clause + qs = Book.objects.values_list("name").annotate( + n_authors=Count("authors") + ).filter( + pages__gt=F("n_authors") + ).values_list("name", flat=True) + # Results should be the same, all Books have more pages than authors + self.assertEqual( + list(qs), list(Book.objects.values_list("name", flat=True)) + ) + + def test_annotation_disjunction(self): + qs = Book.objects.annotate(n_authors=Count("authors")).filter( + Q(n_authors=2) | Q(name="Python Web Development with Django") + ) + self.assertQuerysetEqual( + qs, [ + "Artificial Intelligence: A Modern Approach", + "Python Web Development with Django", + "The Definitive Guide to Django: Web Development Done Right", + ], + attrgetter("name") + ) + + qs = Book.objects.annotate(n_authors=Count("authors")).filter( + Q(name="The Definitive Guide to Django: Web Development Done Right") | (Q(name="Artificial Intelligence: A Modern Approach") & Q(n_authors=3)) + ) + self.assertQuerysetEqual( + qs, [ + "The Definitive Guide to Django: Web Development Done Right", + ], + attrgetter("name") + ) + + qs = Publisher.objects.annotate( + rating_sum=Sum("book__rating"), + book_count=Count("book") + ).filter( + Q(rating_sum__gt=5.5) | Q(rating_sum__isnull=True) + ).order_by('pk') + self.assertQuerysetEqual( + qs, [ + "Apress", + "Prentice Hall", + "Jonno's House of Books", + ], + attrgetter("name") + ) + + qs = Publisher.objects.annotate( + rating_sum=Sum("book__rating"), + book_count=Count("book") + ).filter( + Q(pk__lt=F("book_count")) | Q(rating_sum=None) + ).order_by("pk") + self.assertQuerysetEqual( + qs, [ + "Apress", + "Jonno's House of Books", + ], + attrgetter("name") + ) + + def test_quoting_aggregate_order_by(self): + qs = Book.objects.filter( + name="Python Web Development with Django" + ).annotate( + authorCount=Count("authors") + ).order_by("authorCount") + self.assertQuerysetEqual( + qs, [ + ("Python Web Development with Django", 3), + ], + lambda b: (b.name, b.authorCount) + ) + + if run_stddev_tests(): + def test_stddev(self): + self.assertEqual( + Book.objects.aggregate(StdDev('pages')), + {'pages__stddev': Approximate(311.46, 1)} + ) + + self.assertEqual( + Book.objects.aggregate(StdDev('rating')), + {'rating__stddev': Approximate(0.60, 1)} + ) + + self.assertEqual( + Book.objects.aggregate(StdDev('price')), + {'price__stddev': Approximate(24.16, 2)} + ) + + self.assertEqual( + Book.objects.aggregate(StdDev('pages', sample=True)), + {'pages__stddev': Approximate(341.19, 2)} + ) + + self.assertEqual( + Book.objects.aggregate(StdDev('rating', sample=True)), + {'rating__stddev': Approximate(0.66, 2)} + ) + + self.assertEqual( + Book.objects.aggregate(StdDev('price', sample=True)), + {'price__stddev': Approximate(26.46, 1)} + ) + + self.assertEqual( + Book.objects.aggregate(Variance('pages')), + {'pages__variance': Approximate(97010.80, 1)} + ) + + self.assertEqual( + Book.objects.aggregate(Variance('rating')), + {'rating__variance': Approximate(0.36, 1)} + ) + + self.assertEqual( + Book.objects.aggregate(Variance('price')), + {'price__variance': Approximate(583.77, 1)} + ) + + self.assertEqual( + Book.objects.aggregate(Variance('pages', sample=True)), + {'pages__variance': Approximate(116412.96, 1)} + ) + + self.assertEqual( + Book.objects.aggregate(Variance('rating', sample=True)), + {'rating__variance': Approximate(0.44, 2)} + ) + + self.assertEqual( + Book.objects.aggregate(Variance('price', sample=True)), + {'price__variance': Approximate(700.53, 2)} + ) diff --git a/tests/regressiontests/app_loading/tests.py b/tests/regressiontests/app_loading/tests.py index 111ed4692558..4fb60b27163d 100644 --- a/tests/regressiontests/app_loading/tests.py +++ b/tests/regressiontests/app_loading/tests.py @@ -7,31 +7,28 @@ from django.conf import Settings from django.db.models.loading import cache, load_app -__test__ = {"API_TESTS": """ -Test the globbing of INSTALLED_APPS. ->>> old_sys_path = sys.path ->>> sys.path.append(os.path.dirname(os.path.abspath(__file__))) - ->>> old_tz = os.environ.get("TZ") ->>> settings = Settings('test_settings') - ->>> settings.INSTALLED_APPS -['parent.app', 'parent.app1', 'parent.app_2'] +class InstalledAppsGlobbingTest(TestCase): + def setUp(self): + self.OLD_SYS_PATH = sys.path[:] + sys.path.append(os.path.dirname(os.path.abspath(__file__))) + self.OLD_TZ = os.environ.get("TZ") ->>> sys.path = old_sys_path + def test_globbing(self): + settings = Settings('test_settings') + self.assertEquals(settings.INSTALLED_APPS, ['parent.app', 'parent.app1', 'parent.app_2']) -# Undo a side-effect of installing a new settings object. ->>> if hasattr(time, "tzset") and old_tz: -... os.environ["TZ"] = old_tz -... time.tzset() + def tearDown(self): + sys.path = self.OLD_SYS_PATH + if hasattr(time, "tzset") and self.OLD_TZ: + os.environ["TZ"] = self.OLD_TZ + time.tzset() -"""} class EggLoadingTest(TestCase): def setUp(self): - self.old_path = sys.path + self.old_path = sys.path[:] self.egg_dir = '%s/eggs' % os.path.dirname(__file__) # This test adds dummy applications to the app cache. These @@ -50,28 +47,28 @@ def test_egg1(self): egg_name = '%s/modelapp.egg' % self.egg_dir sys.path.append(egg_name) models = load_app('app_with_models') - self.failIf(models is None) + self.assertFalse(models is None) def test_egg2(self): """Loading an app from an egg that has no models returns no models (and no error)""" egg_name = '%s/nomodelapp.egg' % self.egg_dir sys.path.append(egg_name) models = load_app('app_no_models') - self.failUnless(models is None) + self.assertTrue(models is None) def test_egg3(self): """Models module can be loaded from an app located under an egg's top-level package""" egg_name = '%s/omelet.egg' % self.egg_dir sys.path.append(egg_name) models = load_app('omelet.app_with_models') - self.failIf(models is None) + self.assertFalse(models is None) def test_egg4(self): """Loading an app with no models from under the top-level egg package generates no error""" egg_name = '%s/omelet.egg' % self.egg_dir sys.path.append(egg_name) models = load_app('omelet.app_no_models') - self.failUnless(models is None) + self.assertTrue(models is None) def test_egg5(self): """Loading an app from an egg that has an import error in its models module raises that error""" @@ -83,4 +80,4 @@ def test_egg5(self): except ImportError, e: # Make sure the message is indicating the actual # problem in the broken app. - self.failUnless("modelz" in e.args[0]) + self.assertTrue("modelz" in e.args[0]) diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py index 423bead1adf6..ea7ff96b91d1 100644 --- a/tests/regressiontests/backends/models.py +++ b/tests/regressiontests/backends/models.py @@ -1,5 +1,4 @@ from django.db import models -from django.db import connection class Square(models.Model): root = models.IntegerField() @@ -20,45 +19,19 @@ class SchoolClass(models.Model): day = models.CharField(max_length=9, blank=True) last_updated = models.DateTimeField() -qn = connection.ops.quote_name -__test__ = {'API_TESTS': """ +class Reporter(models.Model): + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) -#4896: Test cursor.executemany ->>> from django.db import connection ->>> cursor = connection.cursor() ->>> opts = Square._meta ->>> f1, f2 = opts.get_field('root'), opts.get_field('square') ->>> query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' -... % (connection.introspection.table_name_converter(opts.db_table), qn(f1.column), qn(f2.column))) ->>> cursor.executemany(query, [(i, i**2) for i in range(-5, 6)]) and None or None ->>> Square.objects.order_by('root') -[, , , , , , , , , , ] - -#4765: executemany with params=[] does nothing ->>> cursor.executemany(query, []) and None or None ->>> Square.objects.count() -11 - -#6254: fetchone, fetchmany, fetchall return strings as unicode objects ->>> Person(first_name="John", last_name="Doe").save() ->>> Person(first_name="Jane", last_name="Doe").save() ->>> Person(first_name="Mary", last_name="Agnelline").save() ->>> Person(first_name="Peter", last_name="Parker").save() ->>> Person(first_name="Clark", last_name="Kent").save() ->>> opts2 = Person._meta ->>> f3, f4 = opts2.get_field('first_name'), opts2.get_field('last_name') ->>> query2 = ('SELECT %s, %s FROM %s ORDER BY %s' -... % (qn(f3.column), qn(f4.column), connection.introspection.table_name_converter(opts2.db_table), -... qn(f3.column))) ->>> cursor.execute(query2) and None or None ->>> cursor.fetchone() -(u'Clark', u'Kent') + def __unicode__(self): + return u"%s %s" % (self.first_name, self.last_name) ->>> list(cursor.fetchmany(2)) -[(u'Jane', u'Doe'), (u'John', u'Doe')] ->>> list(cursor.fetchall()) -[(u'Mary', u'Agnelline'), (u'Peter', u'Parker')] +class Article(models.Model): + headline = models.CharField(max_length=100) + pub_date = models.DateField() + reporter = models.ForeignKey(Reporter) -"""} + def __unicode__(self): + return self.headline diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py index 6a26a608eb3f..10cec8962d41 100644 --- a/tests/regressiontests/backends/tests.py +++ b/tests/regressiontests/backends/tests.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- # Unit and doctests for specific database backends. import datetime -import models import unittest -from django.db import backend, connection, DEFAULT_DB_ALIAS -from django.db.backends.signals import connection_created + from django.conf import settings -from django.test import TestCase +from django.db import backend, connection, DEFAULT_DB_ALIAS, IntegrityError +from django.db.backends.signals import connection_created +from django.db.backends.postgresql import version as pg_version +from django.test import TestCase, TransactionTestCase -class Callproc(unittest.TestCase): +import models + +class OracleChecks(unittest.TestCase): def test_dbms_session(self): # If the backend is Oracle, test that we can call a standard @@ -31,9 +34,6 @@ def test_cursor_var(self): cursor.execute("BEGIN %s := 'X'; END; ", [var]) self.assertEqual(var.getvalue(), 'X') - -class LongString(unittest.TestCase): - def test_long_string(self): # If the backend is Oracle, test that we can save a text longer # than 4000 chars and read it properly @@ -47,6 +47,14 @@ def test_long_string(self): self.assertEquals(long_str, row[0].read()) c.execute('DROP TABLE ltext') + def test_client_encoding(self): + # If the backend is Oracle, test that the client encoding is set + # correctly. This was broken under Cygwin prior to r14781. + if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] == 'django.db.backends.oracle': + c = connection.cursor() # Ensure the connection is initialized. + self.assertEqual(connection.connection.encoding, "UTF-8") + self.assertEqual(connection.connection.nencoding, "UTF-8") + class DateQuotingTest(TestCase): def test_django_date_trunc(self): @@ -88,46 +96,113 @@ def test_bad_parameter_count(self): self.assertRaises(Exception, cursor.executemany, query, [(1,2,3),]) self.assertRaises(Exception, cursor.executemany, query, [(1,),]) +class PostgresVersionTest(TestCase): + def assert_parses(self, version_string, version): + self.assertEqual(pg_version._parse_version(version_string), version) -def connection_created_test(sender, **kwargs): - print 'connection_created signal' - -__test__ = {'API_TESTS': """ -# Check Postgres version parsing ->>> from django.db.backends.postgresql import version as pg_version - ->>> pg_version._parse_version("PostgreSQL 8.3.1 on i386-apple-darwin9.2.2, compiled by GCC i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5478)") -(8, 3, 1) - ->>> pg_version._parse_version("PostgreSQL 8.3.6") -(8, 3, 6) - ->>> pg_version._parse_version("PostgreSQL 8.3") -(8, 3, None) + def test_parsing(self): + self.assert_parses("PostgreSQL 8.3 beta4", (8, 3, None)) + self.assert_parses("PostgreSQL 8.3", (8, 3, None)) + self.assert_parses("EnterpriseDB 8.3", (8, 3, None)) + self.assert_parses("PostgreSQL 8.3.6", (8, 3, 6)) + self.assert_parses("PostgreSQL 8.4beta1", (8, 4, None)) + self.assert_parses("PostgreSQL 8.3.1 on i386-apple-darwin9.2.2, compiled by GCC i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5478)", (8, 3, 1)) ->>> pg_version._parse_version("EnterpriseDB 8.3") -(8, 3, None) +# Unfortunately with sqlite3 the in-memory test database cannot be +# closed, and so it cannot be re-opened during testing, and so we +# sadly disable this test for now. +if settings.DATABASES[DEFAULT_DB_ALIAS]["ENGINE"] != "django.db.backends.sqlite3": + class ConnectionCreatedSignalTest(TestCase): + def test_signal(self): + data = {} + def receiver(sender, connection, **kwargs): + data["connection"] = connection + + connection_created.connect(receiver) + connection.close() + cursor = connection.cursor() + self.assertTrue(data["connection"] is connection) ->>> pg_version._parse_version("PostgreSQL 8.3 beta4") -(8, 3, None) + connection_created.disconnect(receiver) + data.clear() + cursor = connection.cursor() + self.assertTrue(data == {}) ->>> pg_version._parse_version("PostgreSQL 8.4beta1") -(8, 4, None) -"""} +class BackendTestCase(TestCase): + def test_cursor_executemany(self): + #4896: Test cursor.executemany + cursor = connection.cursor() + qn = connection.ops.quote_name + opts = models.Square._meta + f1, f2 = opts.get_field('root'), opts.get_field('square') + query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' + % (connection.introspection.table_name_converter(opts.db_table), qn(f1.column), qn(f2.column))) + cursor.executemany(query, [(i, i**2) for i in range(-5, 6)]) + self.assertEqual(models.Square.objects.count(), 11) + for i in range(-5, 6): + square = models.Square.objects.get(root=i) + self.assertEqual(square.square, i**2) + + #4765: executemany with params=[] does nothing + cursor.executemany(query, []) + self.assertEqual(models.Square.objects.count(), 11) + + +# We don't make these tests conditional because that means we would need to +# check and differentiate between: +# * MySQL+InnoDB, MySQL+MYISAM (something we currently can't do). +# * if sqlite3 (if/once we get #14204 fixed) has referential integrity turned +# on or not, something that would be controlled by runtime support and user +# preference. +# verify if its type is django.database.db.IntegrityError. + +class FkConstraintsTests(TransactionTestCase): + + def setUp(self): + # Create a Reporter. + self.r = models.Reporter.objects.create(first_name='John', last_name='Smith') + + def test_integrity_checks_on_creation(self): + """ + Try to create a model instance that violates a FK constraint. If it + fails it should fail with IntegrityError. + """ + a = models.Article(headline="This is a test", pub_date=datetime.datetime(2005, 7, 27), reporter_id=30) + try: + a.save() + except IntegrityError: + pass -# Unfortunately with sqlite3 the in-memory test database cannot be -# closed, and so it cannot be re-opened during testing, and so we -# sadly disable this test for now. -if settings.DATABASES[DEFAULT_DB_ALIAS]['ENGINE'] != 'django.db.backends.sqlite3': - __test__['API_TESTS'] += """ ->>> connection_created.connect(connection_created_test) ->>> connection.close() # Ensure the connection is closed ->>> cursor = connection.cursor() -connection_created signal ->>> connection_created.disconnect(connection_created_test) ->>> cursor = connection.cursor() -""" - -if __name__ == '__main__': - unittest.main() + def test_integrity_checks_on_update(self): + """ + Try to update a model instance introducing a FK constraint violation. + If it fails it should fail with IntegrityError. + """ + # Create an Article. + models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) + # Retrive it from the DB + a = models.Article.objects.get(headline="Test article") + a.reporter_id = 30 + try: + a.save() + except IntegrityError: + pass + def test_unicode_fetches(self): + #6254: fetchone, fetchmany, fetchall return strings as unicode objects + qn = connection.ops.quote_name + models.Person(first_name="John", last_name="Doe").save() + models.Person(first_name="Jane", last_name="Doe").save() + models.Person(first_name="Mary", last_name="Agnelline").save() + models.Person(first_name="Peter", last_name="Parker").save() + models.Person(first_name="Clark", last_name="Kent").save() + opts2 = models.Person._meta + f3, f4 = opts2.get_field('first_name'), opts2.get_field('last_name') + query2 = ('SELECT %s, %s FROM %s ORDER BY %s' + % (qn(f3.column), qn(f4.column), connection.introspection.table_name_converter(opts2.db_table), + qn(f3.column))) + cursor = connection.cursor() + cursor.execute(query2) + self.assertEqual(cursor.fetchone(), (u'Clark', u'Kent')) + self.assertEqual(list(cursor.fetchmany(2)), [(u'Jane', u'Doe'), (u'John', u'Doe')]) + self.assertEqual(list(cursor.fetchall()), [(u'Mary', u'Agnelline'), (u'Peter', u'Parker')]) diff --git a/tests/regressiontests/bug8245/tests.py b/tests/regressiontests/bug8245/tests.py index e45dd05673fa..5aa4a94e3b8a 100644 --- a/tests/regressiontests/bug8245/tests.py +++ b/tests/regressiontests/bug8245/tests.py @@ -13,7 +13,7 @@ def test_bug_8245(self): try: admin.autodiscover() except Exception, e: - self.failUnlessEqual(str(e), "Bad admin module") + self.assertEqual(str(e), "Bad admin module") else: self.fail( 'autodiscover should have raised a "Bad admin module" error.') @@ -23,7 +23,7 @@ def test_bug_8245(self): try: admin.autodiscover() except Exception, e: - self.failUnlessEqual(str(e), "Bad admin module") + self.assertEqual(str(e), "Bad admin module") else: self.fail( 'autodiscover should have raised a "Bad admin module" error.') diff --git a/tests/regressiontests/builtin_server/tests.py b/tests/regressiontests/builtin_server/tests.py index ad3abc9e9f9d..c3cfef1fe2d2 100644 --- a/tests/regressiontests/builtin_server/tests.py +++ b/tests/regressiontests/builtin_server/tests.py @@ -47,5 +47,5 @@ def test_file_wrapper_no_sendfile(self): err = StringIO() handler = FileWrapperHandler(None, StringIO(), err, env) handler.run(wsgi_app) - self.failIf(handler._used_sendfile) + self.assertFalse(handler._used_sendfile) self.assertEqual(handler.stdout.getvalue().splitlines()[-1],'Hello World!') diff --git a/tests/regressiontests/cache/liberal_backend.py b/tests/regressiontests/cache/liberal_backend.py new file mode 100644 index 000000000000..5c7e3126906a --- /dev/null +++ b/tests/regressiontests/cache/liberal_backend.py @@ -0,0 +1,9 @@ +from django.core.cache.backends.locmem import CacheClass as LocMemCacheClass + +class LiberalKeyValidationMixin(object): + def validate_key(self, key): + pass + +class CacheClass(LiberalKeyValidationMixin, LocMemCacheClass): + pass + diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py index 109374c46cab..0581e4e7d8af 100644 --- a/tests/regressiontests/cache/tests.py +++ b/tests/regressiontests/cache/tests.py @@ -4,20 +4,22 @@ # Uses whatever cache backend is set in the test settings file. import os -import shutil import tempfile import time import unittest +import warnings from django.conf import settings from django.core import management from django.core.cache import get_cache -from django.core.cache.backends.base import InvalidCacheBackendError +from django.core.cache.backends.base import CacheKeyWarning from django.http import HttpResponse, HttpRequest -from django.middleware.cache import FetchFromCacheMiddleware, UpdateCacheMiddleware +from django.middleware.cache import FetchFromCacheMiddleware, UpdateCacheMiddleware, CacheMiddleware +from django.test.utils import get_warnings_state, restore_warnings_state from django.utils import translation from django.utils.cache import patch_vary_headers, get_cache_key, learn_cache_key from django.utils.hashcompat import md5_constructor +from django.views.decorators.cache import cache_page from regressiontests.cache.models import Poll, expensive_calculation # functions/classes for complex data type tests @@ -291,20 +293,48 @@ def test_unicode(self): u'Iñtërnâtiônàlizætiøn': u'Iñtërnâtiônàlizætiøn2', u'ascii': {u'x' : 1 } } + # Test `set` for (key, value) in stuff.items(): self.cache.set(key, value) self.assertEqual(self.cache.get(key), value) + # Test `add` + for (key, value) in stuff.items(): + self.cache.delete(key) + self.cache.add(key, value) + self.assertEqual(self.cache.get(key), value) + + # Test `set_many` + for (key, value) in stuff.items(): + self.cache.delete(key) + self.cache.set_many(stuff) + for (key, value) in stuff.items(): + self.assertEqual(self.cache.get(key), value) + def test_binary_string(self): # Binary strings should be cachable from zlib import compress, decompress value = 'value_to_be_compressed' compressed_value = compress(value) + + # Test set self.cache.set('binary1', compressed_value) compressed_result = self.cache.get('binary1') self.assertEqual(compressed_value, compressed_result) self.assertEqual(value, decompress(compressed_result)) + # Test add + self.cache.add('binary1-add', compressed_value) + compressed_result = self.cache.get('binary1-add') + self.assertEqual(compressed_value, compressed_result) + self.assertEqual(value, decompress(compressed_result)) + + # Test set_many + self.cache.set_many({'binary1-set_many': compressed_value}) + compressed_result = self.cache.get('binary1-set_many') + self.assertEqual(compressed_value, compressed_result) + self.assertEqual(value, decompress(compressed_result)) + def test_set_many(self): # Multiple keys can be set using set_many self.cache.set_many({"key1": "spam", "key2": "eggs"}) @@ -352,21 +382,64 @@ def test_long_timeout(self): self.assertEqual(self.cache.get('key3'), 'sausage') self.assertEqual(self.cache.get('key4'), 'lobster bisque') + def perform_cull_test(self, initial_count, final_count): + """This is implemented as a utility method, because only some of the backends + implement culling. The culling algorithm also varies slightly, so the final + number of entries will vary between backends""" + # Create initial cache key entries. This will overflow the cache, causing a cull + for i in range(1, initial_count): + self.cache.set('cull%d' % i, 'value', 1000) + count = 0 + # Count how many keys are left in the cache. + for i in range(1, initial_count): + if self.cache.has_key('cull%d' % i): + count = count + 1 + self.assertEqual(count, final_count) + + def test_invalid_keys(self): + """ + All the builtin backends (except memcached, see below) should warn on + keys that would be refused by memcached. This encourages portable + caching code without making it too difficult to use production backends + with more liberal key rules. Refs #6447. + + """ + # On Python 2.6+ we could use the catch_warnings context + # manager to test this warning nicely. Since we can't do that + # yet, the cleanest option is to temporarily ask for + # CacheKeyWarning to be raised as an exception. + _warnings_state = get_warnings_state() + warnings.simplefilter("error", CacheKeyWarning) + + try: + # memcached does not allow whitespace or control characters in keys + self.assertRaises(CacheKeyWarning, self.cache.set, 'key with spaces', 'value') + # memcached limits key length to 250 + self.assertRaises(CacheKeyWarning, self.cache.set, 'a' * 251, 'value') + finally: + restore_warnings_state(_warnings_state) + class DBCacheTests(unittest.TestCase, BaseCacheTests): def setUp(self): # Spaces are used in the table name to ensure quoting/escaping is working self._table_name = 'test cache table' management.call_command('createcachetable', self._table_name, verbosity=0, interactive=False) - self.cache = get_cache('db://%s' % self._table_name) + self.cache = get_cache('db://%s?max_entries=30' % self._table_name) def tearDown(self): from django.db import connection cursor = connection.cursor() cursor.execute('DROP TABLE %s' % connection.ops.quote_name(self._table_name)) + def test_cull(self): + self.perform_cull_test(50, 29) + class LocMemCacheTests(unittest.TestCase, BaseCacheTests): def setUp(self): - self.cache = get_cache('locmem://') + self.cache = get_cache('locmem://?max_entries=30') + + def test_cull(self): + self.perform_cull_test(50, 29) # memcached backend isn't guaranteed to be available. # To check the memcached backend, the test settings file will @@ -377,13 +450,29 @@ class MemcachedCacheTests(unittest.TestCase, BaseCacheTests): def setUp(self): self.cache = get_cache(settings.CACHE_BACKEND) + def test_invalid_keys(self): + """ + On memcached, we don't introduce a duplicate key validation + step (for speed reasons), we just let the memcached API + library raise its own exception on bad keys. Refs #6447. + + In order to be memcached-API-library agnostic, we only assert + that a generic exception of some kind is raised. + + """ + # memcached does not allow whitespace or control characters in keys + self.assertRaises(Exception, self.cache.set, 'key with spaces', 'value') + # memcached limits key length to 250 + self.assertRaises(Exception, self.cache.set, 'a' * 251, 'value') + + class FileBasedCacheTests(unittest.TestCase, BaseCacheTests): """ Specific test cases for the file-based cache. """ def setUp(self): self.dirname = tempfile.mkdtemp() - self.cache = get_cache('file://%s' % self.dirname) + self.cache = get_cache('file://%s?max_entries=30' % self.dirname) def test_hashing(self): """Test that keys are hashed into subdirectories correctly""" @@ -406,6 +495,25 @@ def test_subdirectory_removal(self): self.assert_(not os.path.exists(os.path.dirname(keypath))) self.assert_(not os.path.exists(os.path.dirname(os.path.dirname(keypath)))) + def test_cull(self): + self.perform_cull_test(50, 28) + +class CustomCacheKeyValidationTests(unittest.TestCase): + """ + Tests for the ability to mixin a custom ``validate_key`` method to + a custom cache backend that otherwise inherits from a builtin + backend, and override the default key validation. Refs #6447. + + """ + def test_custom_key_validation(self): + cache = get_cache('regressiontests.cache.liberal_backend://') + + # this key is both longer than 250 characters, and has spaces + key = 'some key with spaces' * 15 + val = 'a value' + cache.set(key, val) + self.assertEqual(cache.get(key), val) + class CacheUtils(unittest.TestCase): """TestCase for django.utils.cache functions.""" @@ -568,5 +676,80 @@ def set_cache(request, lang, msg): get_cache_data = FetchFromCacheMiddleware().process_request(request) self.assertEqual(get_cache_data.content, es_message) +def hello_world_view(request, value): + return HttpResponse('Hello World %s' % value) + +class CacheMiddlewareTest(unittest.TestCase): + + def setUp(self): + from django.middleware import cache as cache_middleware_module + + self.cache_middleware_module = cache_middleware_module + self.orig_cache = self.cache_middleware_module.cache + self.orig_cache_middleware_anonymous_only = getattr(settings, 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY', False) + + cache_middleware_module.cache = get_cache("locmem://") + settings.CACHE_MIDDLEWARE_ANONYMOUS_ONLY = False + + def tearDown(self): + self.cache_middleware_module.cache = self.orig_cache + settings.CACHE_MIDDLEWARE_ANONYMOUS_ONLY = self.orig_cache_middleware_anonymous_only + + def test_cache_middleware_anonymous_only_wont_cause_session_access(self): + """ The cache middleware shouldn't cause a session access due to + CACHE_MIDDLEWARE_ANONYMOUS_ONLY if nothing else has accessed the + session. Refs 13283 """ + settings.CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True + + from django.contrib.sessions.middleware import SessionMiddleware + from django.contrib.auth.middleware import AuthenticationMiddleware + + middleware = CacheMiddleware() + session_middleware = SessionMiddleware() + auth_middleware = AuthenticationMiddleware() + + request = HttpRequest() + request.path = '/view_anon/' + request.method = 'GET' + + # Put the request through the request middleware + session_middleware.process_request(request) + auth_middleware.process_request(request) + result = middleware.process_request(request) + self.assertEquals(result, None) + + response = hello_world_view(request, '1') + + # Now put the response through the response middleware + session_middleware.process_response(request, response) + response = middleware.process_response(request, response) + + self.assertEqual(request.session.accessed, False) + + def test_cache_middleware_anonymous_only_with_cache_page(self): + """CACHE_MIDDLEWARE_ANONYMOUS_ONLY should still be effective when used + with the cache_page decorator: the response to a request from an + authenticated user should not be cached.""" + settings.CACHE_MIDDLEWARE_ANONYMOUS_ONLY = True + + request = HttpRequest() + request.path = '/view/' + request.method = 'GET' + + class MockAuthenticatedUser(object): + def is_authenticated(self): + return True + + class MockAccessedSession(object): + accessed = True + + request.user = MockAuthenticatedUser() + request.session = MockAccessedSession() + + response = cache_page(hello_world_view)(request, '1') + + self.assertFalse("Cache-Control" in response) + + if __name__ == '__main__': unittest.main() diff --git a/tests/regressiontests/comment_tests/tests/__init__.py b/tests/regressiontests/comment_tests/tests/__init__.py index 449fea471dd7..88c6f33203db 100644 --- a/tests/regressiontests/comment_tests/tests/__init__.py +++ b/tests/regressiontests/comment_tests/tests/__init__.py @@ -81,6 +81,7 @@ def getValidData(self, obj): return d from regressiontests.comment_tests.tests.app_api_tests import * +from regressiontests.comment_tests.tests.feed_tests import * from regressiontests.comment_tests.tests.model_tests import * from regressiontests.comment_tests.tests.comment_form_tests import * from regressiontests.comment_tests.tests.templatetag_tests import * diff --git a/tests/regressiontests/comment_tests/tests/comment_form_tests.py b/tests/regressiontests/comment_tests/tests/comment_form_tests.py index 142931bfd6b5..8dfcc07c47d4 100644 --- a/tests/regressiontests/comment_tests/tests/comment_form_tests.py +++ b/tests/regressiontests/comment_tests/tests/comment_form_tests.py @@ -1,18 +1,21 @@ import time + from django.conf import settings -from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm +from django.contrib.comments.models import Comment +from django.utils.hashcompat import sha_constructor + from regressiontests.comment_tests.models import Article from regressiontests.comment_tests.tests import CommentTestCase -class CommentFormTests(CommentTestCase): +class CommentFormTests(CommentTestCase): def testInit(self): f = CommentForm(Article.objects.get(pk=1)) self.assertEqual(f.initial['content_type'], str(Article._meta)) self.assertEqual(f.initial['object_pk'], "1") - self.failIfEqual(f.initial['security_hash'], None) - self.failIfEqual(f.initial['timestamp'], None) + self.assertNotEqual(f.initial['security_hash'], None) + self.assertNotEqual(f.initial['timestamp'], None) def testValidPost(self): a = Article.objects.get(pk=1) @@ -25,7 +28,7 @@ def tamperWithForm(self, **kwargs): d = self.getValidData(a) d.update(kwargs) f = CommentForm(Article.objects.get(pk=1), data=d) - self.failIf(f.is_valid()) + self.assertFalse(f.is_valid()) return f def testHoneypotTampering(self): @@ -70,12 +73,12 @@ def testProfanities(self): # Try with COMMENTS_ALLOW_PROFANITIES off settings.COMMENTS_ALLOW_PROFANITIES = False f = CommentForm(a, data=dict(d, comment="What a rooster!")) - self.failIf(f.is_valid()) + self.assertFalse(f.is_valid()) # Now with COMMENTS_ALLOW_PROFANITIES on settings.COMMENTS_ALLOW_PROFANITIES = True f = CommentForm(a, data=dict(d, comment="What a rooster!")) - self.failUnless(f.is_valid()) + self.assertTrue(f.is_valid()) # Restore settings settings.PROFANITIES_LIST, settings.COMMENTS_ALLOW_PROFANITIES = saved diff --git a/tests/regressiontests/comment_tests/tests/comment_view_tests.py b/tests/regressiontests/comment_tests/tests/comment_view_tests.py index f02dca4a8982..5e791966aecc 100644 --- a/tests/regressiontests/comment_tests/tests/comment_view_tests.py +++ b/tests/regressiontests/comment_tests/tests/comment_view_tests.py @@ -160,13 +160,18 @@ def receive(sender, **kwargs): # Connect signals and keep track of handled ones received_signals = [] - excepted_signals = [signals.comment_will_be_posted, signals.comment_was_posted] - for signal in excepted_signals: + expected_signals = [ + signals.comment_will_be_posted, signals.comment_was_posted + ] + for signal in expected_signals: signal.connect(receive) # Post a comment and check the signals self.testCreateValidComment() - self.assertEqual(received_signals, excepted_signals) + self.assertEqual(received_signals, expected_signals) + + for signal in expected_signals: + signal.disconnect(receive) def testWillBePostedSignal(self): """ @@ -194,7 +199,7 @@ def receive(sender, **kwargs): signals.comment_will_be_posted.connect(receive) self.testCreateValidComment() c = Comment.objects.all()[0] - self.failIf(c.is_public) + self.assertFalse(c.is_public) def testCommentNext(self): """Test the different "next" actions the comment view can take""" @@ -203,14 +208,14 @@ def testCommentNext(self): response = self.client.post("/post/", data) location = response["Location"] match = post_redirect_re.match(location) - self.failUnless(match != None, "Unexpected redirect location: %s" % location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) data["next"] = "/somewhere/else/" data["comment"] = "This is another comment" response = self.client.post("/post/", data) location = response["Location"] match = re.search(r"^http://testserver/somewhere/else/\?c=\d+$", location) - self.failUnless(match != None, "Unexpected redirect location: %s" % location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) def testCommentDoneView(self): a = Article.objects.get(pk=1) @@ -218,7 +223,7 @@ def testCommentDoneView(self): response = self.client.post("/post/", data) location = response["Location"] match = post_redirect_re.match(location) - self.failUnless(match != None, "Unexpected redirect location: %s" % location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) pk = int(match.group('pk')) response = self.client.get(location) self.assertTemplateUsed(response, "comments/posted.html") @@ -235,7 +240,7 @@ def testCommentNextWithQueryString(self): response = self.client.post("/post/", data) location = response["Location"] match = re.search(r"^http://testserver/somewhere/else/\?foo=bar&c=\d+$", location) - self.failUnless(match != None, "Unexpected redirect location: %s" % location) + self.assertTrue(match != None, "Unexpected redirect location: %s" % location) def testCommentPostRedirectWithInvalidIntegerPK(self): """ @@ -252,3 +257,26 @@ def testCommentPostRedirectWithInvalidIntegerPK(self): response = self.client.get(broken_location) self.assertEqual(response.status_code, 200) + def testCommentNextWithQueryStringAndAnchor(self): + """ + The `next` key needs to handle already having an anchor. Refs #13411. + """ + # With a query string also. + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["next"] = "/somewhere/else/?foo=bar#baz" + data["comment"] = "This is another comment" + response = self.client.post("/post/", data) + location = response["Location"] + match = re.search(r"^http://testserver/somewhere/else/\?foo=bar&c=\d+#baz$", location) + self.failUnless(match != None, "Unexpected redirect location: %s" % location) + + # Without a query string + a = Article.objects.get(pk=1) + data = self.getValidData(a) + data["next"] = "/somewhere/else/#baz" + data["comment"] = "This is another comment" + response = self.client.post("/post/", data) + location = response["Location"] + match = re.search(r"^http://testserver/somewhere/else/\?c=\d+#baz$", location) + self.failUnless(match != None, "Unexpected redirect location: %s" % location) diff --git a/tests/regressiontests/comment_tests/tests/feed_tests.py b/tests/regressiontests/comment_tests/tests/feed_tests.py new file mode 100644 index 000000000000..0bc6618d9a8e --- /dev/null +++ b/tests/regressiontests/comment_tests/tests/feed_tests.py @@ -0,0 +1,33 @@ +import warnings + +from django.test.utils import get_warnings_state, restore_warnings_state + +from regressiontests.comment_tests.tests import CommentTestCase + + +class CommentFeedTests(CommentTestCase): + urls = 'regressiontests.comment_tests.urls' + feed_url = '/rss/comments/' + + def test_feed(self): + response = self.client.get(self.feed_url) + self.assertEquals(response.status_code, 200) + self.assertEquals(response['Content-Type'], 'application/rss+xml') + self.assertContains(response, '') + self.assertContains(response, 'example.com comments') + self.assertContains(response, 'http://example.com/') + self.assertContains(response, '') + + +class LegacyCommentFeedTests(CommentFeedTests): + feed_url = '/rss/legacy/comments/' + + def setUp(self): + self._warnings_state = get_warnings_state() + warnings.filterwarnings("ignore", category=DeprecationWarning, + module='django.contrib.syndication.views') + warnings.filterwarnings("ignore", category=DeprecationWarning, + module='django.contrib.syndication.feeds') + + def tearDown(self): + restore_warnings_state(self._warnings_state) diff --git a/tests/regressiontests/comment_tests/tests/model_tests.py b/tests/regressiontests/comment_tests/tests/model_tests.py index 17797bb7a6e7..2cbf66f07ed5 100644 --- a/tests/regressiontests/comment_tests/tests/model_tests.py +++ b/tests/regressiontests/comment_tests/tests/model_tests.py @@ -1,12 +1,13 @@ from django.contrib.comments.models import Comment + from regressiontests.comment_tests.models import Author, Article from regressiontests.comment_tests.tests import CommentTestCase -class CommentModelTests(CommentTestCase): +class CommentModelTests(CommentTestCase): def testSave(self): for c in self.createSomeComments(): - self.failIfEqual(c.submit_date, None) + self.assertNotEqual(c.submit_date, None) def testUserProperties(self): c1, c2, c3, c4 = self.createSomeComments() diff --git a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py index c9b50e298314..e5094ab0cce9 100644 --- a/tests/regressiontests/comment_tests/tests/moderation_view_tests.py +++ b/tests/regressiontests/comment_tests/tests/moderation_view_tests.py @@ -1,8 +1,10 @@ -from django.contrib.comments.models import Comment, CommentFlag from django.contrib.auth.models import User, Permission +from django.contrib.comments import signals +from django.contrib.comments.models import Comment, CommentFlag from django.contrib.contenttypes.models import ContentType + from regressiontests.comment_tests.tests import CommentTestCase -from django.contrib.comments import signals + class FlagViewTests(CommentTestCase): @@ -35,7 +37,7 @@ def testFlagPostTwice(self): def testFlagAnon(self): """GET/POST the flag view while not logged in: redirect to log in.""" comments = self.createSomeComments() - pk = comments[0].pk + pk = comments[0].pk response = self.client.get("/flag/%d/" % pk) self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/flag/%d/" % pk) response = self.client.post("/flag/%d/" % pk) @@ -43,7 +45,7 @@ def testFlagAnon(self): def testFlaggedView(self): comments = self.createSomeComments() - pk = comments[0].pk + pk = comments[0].pk response = self.client.get("/flagged/", data={"c":pk}) self.assertTemplateUsed(response, "comments/flagged.html") @@ -75,7 +77,7 @@ class DeleteViewTests(CommentTestCase): def testDeletePermissions(self): """The delete view should only be accessible to 'moderators'""" comments = self.createSomeComments() - pk = comments[0].pk + pk = comments[0].pk self.client.login(username="normaluser", password="normaluser") response = self.client.get("/delete/%d/" % pk) self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/delete/%d/" % pk) @@ -93,7 +95,7 @@ def testDeletePost(self): response = self.client.post("/delete/%d/" % pk) self.assertEqual(response["Location"], "http://testserver/deleted/?c=%d" % pk) c = Comment.objects.get(pk=pk) - self.failUnless(c.is_removed) + self.assertTrue(c.is_removed) self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_DELETION, user__username="normaluser").count(), 1) def testDeleteSignals(self): @@ -110,7 +112,7 @@ def receive(sender, **kwargs): def testDeletedView(self): comments = self.createSomeComments() - pk = comments[0].pk + pk = comments[0].pk response = self.client.get("/deleted/", data={"c":pk}) self.assertTemplateUsed(response, "comments/deleted.html") @@ -119,7 +121,7 @@ class ApproveViewTests(CommentTestCase): def testApprovePermissions(self): """The delete view should only be accessible to 'moderators'""" comments = self.createSomeComments() - pk = comments[0].pk + pk = comments[0].pk self.client.login(username="normaluser", password="normaluser") response = self.client.get("/approve/%d/" % pk) self.assertEqual(response["Location"], "http://testserver/accounts/login/?next=/approve/%d/" % pk) @@ -138,7 +140,7 @@ def testApprovePost(self): response = self.client.post("/approve/%d/" % c1.pk) self.assertEqual(response["Location"], "http://testserver/approved/?c=%d" % c1.pk) c = Comment.objects.get(pk=c1.pk) - self.failUnless(c.is_public) + self.assertTrue(c.is_public) self.assertEqual(c.flags.filter(flag=CommentFlag.MODERATOR_APPROVAL, user__username="normaluser").count(), 1) def testApproveSignals(self): @@ -155,21 +157,21 @@ def receive(sender, **kwargs): def testApprovedView(self): comments = self.createSomeComments() - pk = comments[0].pk + pk = comments[0].pk response = self.client.get("/approved/", data={"c":pk}) self.assertTemplateUsed(response, "comments/approved.html") class AdminActionsTests(CommentTestCase): urls = "regressiontests.comment_tests.urls_admin" - + def setUp(self): super(AdminActionsTests, self).setUp() - + # Make "normaluser" a moderator u = User.objects.get(username="normaluser") u.is_staff = True perms = Permission.objects.filter( - content_type__app_label = 'comments', + content_type__app_label = 'comments', codename__endswith = 'comment' ) for perm in perms: @@ -188,3 +190,14 @@ def testActionsModerator(self): self.client.login(username="normaluser", password="normaluser") response = self.client.get("/admin/comments/comment/") self.assertEquals("approve_comments" in response.content, True) + + def testActionsDisabledDelete(self): + "Tests a CommentAdmin where 'delete_selected' has been disabled." + comments = self.createSomeComments() + self.client.login(username="normaluser", password="normaluser") + response = self.client.get('/admin2/comments/comment/') + self.assertEqual(response.status_code, 200) + self.assert_( + '
        - - - -Empty dictionaries are valid, too. ->>> p = Person({}) ->>> p.is_bound -True ->>> p.errors['first_name'] -[u'This field is required.'] ->>> p.errors['last_name'] -[u'This field is required.'] ->>> p.errors['birthday'] -[u'This field is required.'] ->>> p.is_valid() -False ->>> p.cleaned_data -Traceback (most recent call last): -... -AttributeError: 'Person' object has no attribute 'cleaned_data' ->>> print p - - - ->>> print p.as_table() - - - ->>> print p.as_ul() -
        • This field is required.
      • -
        • This field is required.
      • -
        • This field is required.
      • ->>> print p.as_p() -
        • This field is required.
        -

        -
        • This field is required.
        -

        -
        • This field is required.
        -

        - -If you don't pass any values to the Form's __init__(), or if you pass None, -the Form will be considered unbound and won't do any validation. Form.errors -will be an empty dictionary *but* Form.is_valid() will return False. ->>> p = Person() ->>> p.is_bound -False ->>> p.errors -{} ->>> p.is_valid() -False ->>> p.cleaned_data -Traceback (most recent call last): -... -AttributeError: 'Person' object has no attribute 'cleaned_data' ->>> print p -
        - - ->>> print p.as_table() - - - ->>> print p.as_ul() -
      • -
      • -
      • ->>> print p.as_p() -

        -

        -

        - -Unicode values are handled properly. ->>> p = Person({'first_name': u'John', 'last_name': u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111', 'birthday': '1940-10-9'}) ->>> p.as_table() -u'
        \n\n' ->>> p.as_ul() -u'
      • \n
      • \n
      • ' ->>> p.as_p() -u'

        \n

        \n

        ' - ->>> p = Person({'last_name': u'Lennon'}) ->>> p.errors['first_name'] -[u'This field is required.'] ->>> p.errors['birthday'] -[u'This field is required.'] ->>> p.is_valid() -False ->>> p.errors.as_ul() -u'
        • first_name
          • This field is required.
        • birthday
          • This field is required.
        ' ->>> print p.errors.as_text() -* first_name - * This field is required. -* birthday - * This field is required. ->>> p.cleaned_data -Traceback (most recent call last): -... -AttributeError: 'Person' object has no attribute 'cleaned_data' ->>> p['first_name'].errors -[u'This field is required.'] ->>> p['first_name'].errors.as_ul() -u'
        • This field is required.
        ' ->>> p['first_name'].errors.as_text() -u'* This field is required.' - ->>> p = Person() ->>> print p['first_name'] - ->>> print p['last_name'] - ->>> print p['birthday'] - - -cleaned_data will always *only* contain a key for fields defined in the -Form, even if you pass extra data when you define the Form. In this -example, we pass a bunch of extra fields to the form constructor, -but cleaned_data contains only the form's fields. ->>> data = {'first_name': u'John', 'last_name': u'Lennon', 'birthday': u'1940-10-9', 'extra1': 'hello', 'extra2': 'hello'} ->>> p = Person(data) ->>> p.is_valid() -True ->>> p.cleaned_data['first_name'] -u'John' ->>> p.cleaned_data['last_name'] -u'Lennon' ->>> p.cleaned_data['birthday'] -datetime.date(1940, 10, 9) - - -cleaned_data will include a key and value for *all* fields defined in the Form, -even if the Form's data didn't include a value for fields that are not -required. In this example, the data dictionary doesn't include a value for the -"nick_name" field, but cleaned_data includes it. For CharFields, it's set to the -empty string. ->>> class OptionalPersonForm(Form): -... first_name = CharField() -... last_name = CharField() -... nick_name = CharField(required=False) ->>> data = {'first_name': u'John', 'last_name': u'Lennon'} ->>> f = OptionalPersonForm(data) ->>> f.is_valid() -True ->>> f.cleaned_data['nick_name'] -u'' ->>> f.cleaned_data['first_name'] -u'John' ->>> f.cleaned_data['last_name'] -u'Lennon' - -For DateFields, it's set to None. ->>> class OptionalPersonForm(Form): -... first_name = CharField() -... last_name = CharField() -... birth_date = DateField(required=False) ->>> data = {'first_name': u'John', 'last_name': u'Lennon'} ->>> f = OptionalPersonForm(data) ->>> f.is_valid() -True ->>> print f.cleaned_data['birth_date'] -None ->>> f.cleaned_data['first_name'] -u'John' ->>> f.cleaned_data['last_name'] -u'Lennon' - -"auto_id" tells the Form to add an "id" attribute to each form element. -If it's a string that contains '%s', Django will use that as a format string -into which the field's name will be inserted. It will also put a
        - - ->>> print p.as_ul() -
      • -
      • -
      • ->>> print p.as_p() -

        -

        -

        - -If auto_id is any True value whose str() does not contain '%s', the "id" -attribute will be the name of the field. ->>> p = Person(auto_id=True) ->>> print p.as_ul() -
      • -
      • -
      • - -If auto_id is any False value, an "id" attribute won't be output unless it -was manually entered. ->>> p = Person(auto_id=False) ->>> print p.as_ul() -
      • First name:
      • -
      • Last name:
      • -
      • Birthday:
      • - -In this example, auto_id is False, but the "id" attribute for the "first_name" -field is given. Also note that field gets a
        - ->>> print f.as_ul() -
      • Name:
      • -
      • Language:
          -
        • -
        • -
      • - -Regarding auto_id and
        - ->>> print f.as_ul() -
      • -
        • -
        • -
        • -
      • ->>> print f.as_p() -

        -

          -
        • -
        • -

        - -MultipleChoiceField is a special case, as its data is required to be a list: ->>> class SongForm(Form): -... name = CharField() -... composers = MultipleChoiceField() ->>> f = SongForm(auto_id=False) ->>> print f['composers'] - ->>> class SongForm(Form): -... name = CharField() -... composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')]) ->>> f = SongForm(auto_id=False) ->>> print f['composers'] - ->>> f = SongForm({'name': 'Yesterday', 'composers': ['P']}, auto_id=False) ->>> print f['name'] - ->>> print f['composers'] - - -MultipleChoiceField rendered as_hidden() is a special case. Because it can -have multiple values, its as_hidden() renders multiple -tags. ->>> f = SongForm({'name': 'Yesterday', 'composers': ['P']}, auto_id=False) ->>> print f['composers'].as_hidden() - ->>> f = SongForm({'name': 'From Me To You', 'composers': ['P', 'J']}, auto_id=False) ->>> print f['composers'].as_hidden() - - - -MultipleChoiceField can also be used with the CheckboxSelectMultiple widget. ->>> class SongForm(Form): -... name = CharField() -... composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=CheckboxSelectMultiple) ->>> f = SongForm(auto_id=False) ->>> print f['composers'] -
          -
        • -
        • -
        ->>> f = SongForm({'composers': ['J']}, auto_id=False) ->>> print f['composers'] -
          -
        • -
        • -
        ->>> f = SongForm({'composers': ['J', 'P']}, auto_id=False) ->>> print f['composers'] -
          -
        • -
        • -
        - -Regarding auto_id, CheckboxSelectMultiple is a special case. Each checkbox -gets a distinct ID, formed by appending an underscore plus the checkbox's -zero-based index. ->>> f = SongForm(auto_id='%s_id') ->>> print f['composers'] -
          -
        • -
        • -
        - -Data for a MultipleChoiceField should be a list. QueryDict, MultiValueDict and -MergeDict (when created as a merge of MultiValueDicts) conveniently work with -this. ->>> data = {'name': 'Yesterday', 'composers': ['J', 'P']} ->>> f = SongForm(data) ->>> f.errors -{} ->>> from django.http import QueryDict ->>> data = QueryDict('name=Yesterday&composers=J&composers=P') ->>> f = SongForm(data) ->>> f.errors -{} ->>> from django.utils.datastructures import MultiValueDict ->>> data = MultiValueDict(dict(name=['Yesterday'], composers=['J', 'P'])) ->>> f = SongForm(data) ->>> f.errors -{} ->>> from django.utils.datastructures import MergeDict ->>> data = MergeDict(MultiValueDict(dict(name=['Yesterday'], composers=['J', 'P']))) ->>> f = SongForm(data) ->>> f.errors -{} - -The MultipleHiddenInput widget renders multiple values as hidden fields. ->>> class SongFormHidden(Form): -... name = CharField() -... composers = MultipleChoiceField(choices=[('J', 'John Lennon'), ('P', 'Paul McCartney')], widget=MultipleHiddenInput) ->>> f = SongFormHidden(MultiValueDict(dict(name=['Yesterday'], composers=['J', 'P'])), auto_id=False) ->>> print f.as_ul() -
      • Name: -
      • - -When using CheckboxSelectMultiple, the framework expects a list of input and -returns a list of input. ->>> f = SongForm({'name': 'Yesterday'}, auto_id=False) ->>> f.errors['composers'] -[u'This field is required.'] ->>> f = SongForm({'name': 'Yesterday', 'composers': ['J']}, auto_id=False) ->>> f.errors -{} ->>> f.cleaned_data['composers'] -[u'J'] ->>> f.cleaned_data['name'] -u'Yesterday' ->>> f = SongForm({'name': 'Yesterday', 'composers': ['J', 'P']}, auto_id=False) ->>> f.errors -{} ->>> f.cleaned_data['composers'] -[u'J', u'P'] ->>> f.cleaned_data['name'] -u'Yesterday' - -Validation errors are HTML-escaped when output as HTML. ->>> from django.utils.safestring import mark_safe ->>> class EscapingForm(Form): -... special_name = CharField(label="Special Field") -... special_safe_name = CharField(label=mark_safe("Special Field")) -... def clean_special_name(self): -... raise ValidationError("Something's wrong with '%s'" % self.cleaned_data['special_name']) -... def clean_special_safe_name(self): -... raise ValidationError(mark_safe("'%s' is a safe string" % self.cleaned_data['special_safe_name'])) - ->>> f = EscapingForm({'special_name': "Nothing to escape", 'special_safe_name': "Nothing to escape"}, auto_id=False) ->>> print f -
        - ->>> f = EscapingForm( -... {'special_name': "Should escape < & > and ", -... 'special_safe_name': "Do not escape"}, auto_id=False) ->>> print f - - - -""" + \ -r""" # [This concatenation is to keep the string below the jython's 32K limit]. -# Validating multiple fields in relation to another ########################### - -There are a couple of ways to do multiple-field validation. If you want the -validation message to be associated with a particular field, implement the -clean_XXX() method on the Form, where XXX is the field name. As in -Field.clean(), the clean_XXX() method should return the cleaned value. In the -clean_XXX() method, you have access to self.cleaned_data, which is a dictionary -of all the data that has been cleaned *so far*, in order by the fields, -including the current field (e.g., the field XXX if you're in clean_XXX()). ->>> class UserRegistration(Form): -... username = CharField(max_length=10) -... password1 = CharField(widget=PasswordInput) -... password2 = CharField(widget=PasswordInput) -... def clean_password2(self): -... if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: -... raise ValidationError(u'Please make sure your passwords match.') -... return self.cleaned_data['password2'] ->>> f = UserRegistration(auto_id=False) ->>> f.errors -{} ->>> f = UserRegistration({}, auto_id=False) ->>> f.errors['username'] -[u'This field is required.'] ->>> f.errors['password1'] -[u'This field is required.'] ->>> f.errors['password2'] -[u'This field is required.'] ->>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'}, auto_id=False) ->>> f.errors['password2'] -[u'Please make sure your passwords match.'] ->>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'}, auto_id=False) ->>> f.errors -{} ->>> f.cleaned_data['username'] -u'adrian' ->>> f.cleaned_data['password1'] -u'foo' ->>> f.cleaned_data['password2'] -u'foo' - -Another way of doing multiple-field validation is by implementing the -Form's clean() method. If you do this, any ValidationError raised by that -method will not be associated with a particular field; it will have a -special-case association with the field named '__all__'. -Note that in Form.clean(), you have access to self.cleaned_data, a dictionary of -all the fields/values that have *not* raised a ValidationError. Also note -Form.clean() is required to return a dictionary of all clean data. ->>> class UserRegistration(Form): -... username = CharField(max_length=10) -... password1 = CharField(widget=PasswordInput) -... password2 = CharField(widget=PasswordInput) -... def clean(self): -... if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: -... raise ValidationError(u'Please make sure your passwords match.') -... return self.cleaned_data ->>> f = UserRegistration(auto_id=False) ->>> f.errors -{} ->>> f = UserRegistration({}, auto_id=False) ->>> print f.as_table() - - - ->>> f.errors['username'] -[u'This field is required.'] ->>> f.errors['password1'] -[u'This field is required.'] ->>> f.errors['password2'] -[u'This field is required.'] ->>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'bar'}, auto_id=False) ->>> f.errors['__all__'] -[u'Please make sure your passwords match.'] ->>> print f.as_table() - - - - ->>> print f.as_ul() -
        • Please make sure your passwords match.
      • -
      • Username:
      • -
      • Password1:
      • -
      • Password2:
      • ->>> f = UserRegistration({'username': 'adrian', 'password1': 'foo', 'password2': 'foo'}, auto_id=False) ->>> f.errors -{} ->>> f.cleaned_data['username'] -u'adrian' ->>> f.cleaned_data['password1'] -u'foo' ->>> f.cleaned_data['password2'] -u'foo' - -# Dynamic construction ######################################################## - -It's possible to construct a Form dynamically by adding to the self.fields -dictionary in __init__(). Don't forget to call Form.__init__() within the -subclass' __init__(). ->>> class Person(Form): -... first_name = CharField() -... last_name = CharField() -... def __init__(self, *args, **kwargs): -... super(Person, self).__init__(*args, **kwargs) -... self.fields['birthday'] = DateField() ->>> p = Person(auto_id=False) ->>> print p -
        - - - -Instances of a dynamic Form do not persist fields from one Form instance to -the next. ->>> class MyForm(Form): -... def __init__(self, data=None, auto_id=False, field_list=[]): -... Form.__init__(self, data, auto_id=auto_id) -... for field in field_list: -... self.fields[field[0]] = field[1] ->>> field_list = [('field1', CharField()), ('field2', CharField())] ->>> my_form = MyForm(field_list=field_list) ->>> print my_form - - ->>> field_list = [('field3', CharField()), ('field4', CharField())] ->>> my_form = MyForm(field_list=field_list) ->>> print my_form - - - ->>> class MyForm(Form): -... default_field_1 = CharField() -... default_field_2 = CharField() -... def __init__(self, data=None, auto_id=False, field_list=[]): -... Form.__init__(self, data, auto_id=auto_id) -... for field in field_list: -... self.fields[field[0]] = field[1] ->>> field_list = [('field1', CharField()), ('field2', CharField())] ->>> my_form = MyForm(field_list=field_list) ->>> print my_form - - - - ->>> field_list = [('field3', CharField()), ('field4', CharField())] ->>> my_form = MyForm(field_list=field_list) ->>> print my_form - - - - - -Similarly, changes to field attributes do not persist from one Form instance -to the next. ->>> class Person(Form): -... first_name = CharField(required=False) -... last_name = CharField(required=False) -... def __init__(self, names_required=False, *args, **kwargs): -... super(Person, self).__init__(*args, **kwargs) -... if names_required: -... self.fields['first_name'].required = True -... self.fields['first_name'].widget.attrs['class'] = 'required' -... self.fields['last_name'].required = True -... self.fields['last_name'].widget.attrs['class'] = 'required' ->>> f = Person(names_required=False) ->>> f['first_name'].field.required, f['last_name'].field.required -(False, False) ->>> f['first_name'].field.widget.attrs, f['last_name'].field.widget.attrs -({}, {}) ->>> f = Person(names_required=True) ->>> f['first_name'].field.required, f['last_name'].field.required -(True, True) ->>> f['first_name'].field.widget.attrs, f['last_name'].field.widget.attrs -({'class': 'required'}, {'class': 'required'}) ->>> f = Person(names_required=False) ->>> f['first_name'].field.required, f['last_name'].field.required -(False, False) ->>> f['first_name'].field.widget.attrs, f['last_name'].field.widget.attrs -({}, {}) ->>> class Person(Form): -... first_name = CharField(max_length=30) -... last_name = CharField(max_length=30) -... def __init__(self, name_max_length=None, *args, **kwargs): -... super(Person, self).__init__(*args, **kwargs) -... if name_max_length: -... self.fields['first_name'].max_length = name_max_length -... self.fields['last_name'].max_length = name_max_length ->>> f = Person(name_max_length=None) ->>> f['first_name'].field.max_length, f['last_name'].field.max_length -(30, 30) ->>> f = Person(name_max_length=20) ->>> f['first_name'].field.max_length, f['last_name'].field.max_length -(20, 20) ->>> f = Person(name_max_length=None) ->>> f['first_name'].field.max_length, f['last_name'].field.max_length -(30, 30) - -HiddenInput widgets are displayed differently in the as_table(), as_ul() -and as_p() output of a Form -- their verbose names are not displayed, and a -separate row is not displayed. They're displayed in the last row of the -form, directly after that row's form element. ->>> class Person(Form): -... first_name = CharField() -... last_name = CharField() -... hidden_text = CharField(widget=HiddenInput) -... birthday = DateField() ->>> p = Person(auto_id=False) ->>> print p - - - ->>> print p.as_ul() -
      • First name:
      • -
      • Last name:
      • -
      • Birthday:
      • ->>> print p.as_p() -

        First name:

        -

        Last name:

        -

        Birthday:

        - -With auto_id set, a HiddenInput still gets an ID, but it doesn't get a label. ->>> p = Person(auto_id='id_%s') ->>> print p -
        - - ->>> print p.as_ul() -
      • -
      • -
      • ->>> print p.as_p() -

        -

        -

        - -If a field with a HiddenInput has errors, the as_table() and as_ul() output -will include the error message(s) with the text "(Hidden field [fieldname]) " -prepended. This message is displayed at the top of the output, regardless of -its field's order in the form. ->>> p = Person({'first_name': 'John', 'last_name': 'Lennon', 'birthday': '1940-10-9'}, auto_id=False) ->>> print p -
        - - - ->>> print p.as_ul() -
        • (Hidden field hidden_text) This field is required.
      • -
      • First name:
      • -
      • Last name:
      • -
      • Birthday:
      • ->>> print p.as_p() -
        • (Hidden field hidden_text) This field is required.
        -

        First name:

        -

        Last name:

        -

        Birthday:

        - -A corner case: It's possible for a form to have only HiddenInputs. ->>> class TestForm(Form): -... foo = CharField(widget=HiddenInput) -... bar = CharField(widget=HiddenInput) ->>> p = TestForm(auto_id=False) ->>> print p.as_table() - ->>> print p.as_ul() - ->>> print p.as_p() - - -A Form's fields are displayed in the same order in which they were defined. ->>> class TestForm(Form): -... field1 = CharField() -... field2 = CharField() -... field3 = CharField() -... field4 = CharField() -... field5 = CharField() -... field6 = CharField() -... field7 = CharField() -... field8 = CharField() -... field9 = CharField() -... field10 = CharField() -... field11 = CharField() -... field12 = CharField() -... field13 = CharField() -... field14 = CharField() ->>> p = TestForm(auto_id=False) ->>> print p -
        - - - - - - - - - - - - - - -Some Field classes have an effect on the HTML attributes of their associated -Widget. If you set max_length in a CharField and its associated widget is -either a TextInput or PasswordInput, then the widget's rendered HTML will -include the "maxlength" attribute. ->>> class UserRegistration(Form): -... username = CharField(max_length=10) # uses TextInput by default -... password = CharField(max_length=10, widget=PasswordInput) -... realname = CharField(max_length=10, widget=TextInput) # redundantly define widget, just to test -... address = CharField() # no max_length defined here ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • -
      • Realname:
      • -
      • Address:
      • - -If you specify a custom "attrs" that includes the "maxlength" attribute, -the Field's max_length attribute will override whatever "maxlength" you specify -in "attrs". ->>> class UserRegistration(Form): -... username = CharField(max_length=10, widget=TextInput(attrs={'maxlength': 20})) -... password = CharField(max_length=10, widget=PasswordInput) ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • - -# Specifying labels ########################################################### - -You can specify the label for a field by using the 'label' argument to a Field -class. If you don't specify 'label', Django will use the field name with -underscores converted to spaces, and the initial letter capitalized. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, label='Your username') -... password1 = CharField(widget=PasswordInput) -... password2 = CharField(widget=PasswordInput, label='Password (again)') ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Your username:
      • -
      • Password1:
      • -
      • Password (again):
      • - -Labels for as_* methods will only end in a colon if they don't end in other -punctuation already. ->>> class Questions(Form): -... q1 = CharField(label='The first question') -... q2 = CharField(label='What is your name?') -... q3 = CharField(label='The answer to life is:') -... q4 = CharField(label='Answer this question!') -... q5 = CharField(label='The last question. Period.') ->>> print Questions(auto_id=False).as_p() -

        The first question:

        -

        What is your name?

        -

        The answer to life is:

        -

        Answer this question!

        -

        The last question. Period.

        ->>> print Questions().as_p() -

        -

        -

        -

        -

        - -A label can be a Unicode object or a bytestring with special characters. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, label='ŠĐĆŽćžšđ') -... password = CharField(widget=PasswordInput, label=u'\u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111') ->>> p = UserRegistration(auto_id=False) ->>> p.as_ul() -u'
      • \u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111:
      • \n
      • \u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111:
      • ' - -If a label is set to the empty string for a field, that field won't get a label. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, label='') -... password = CharField(widget=PasswordInput) ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • -
      • Password:
      • ->>> p = UserRegistration(auto_id='id_%s') ->>> print p.as_ul() -
      • -
      • - -If label is None, Django will auto-create the label from the field name. This -is default behavior. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, label=None) -... password = CharField(widget=PasswordInput) ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • ->>> p = UserRegistration(auto_id='id_%s') ->>> print p.as_ul() -
      • -
      • - - -# Label Suffix ################################################################ - -You can specify the 'label_suffix' argument to a Form class to modify the -punctuation symbol used at the end of a label. By default, the colon (:) is -used, and is only appended to the label if the label doesn't already end with a -punctuation symbol: ., !, ? or :. If you specify a different suffix, it will -be appended regardless of the last character of the label. - ->>> class FavoriteForm(Form): -... color = CharField(label='Favorite color?') -... animal = CharField(label='Favorite animal') -... ->>> f = FavoriteForm(auto_id=False) ->>> print f.as_ul() -
      • Favorite color?
      • -
      • Favorite animal:
      • ->>> f = FavoriteForm(auto_id=False, label_suffix='?') ->>> print f.as_ul() -
      • Favorite color?
      • -
      • Favorite animal?
      • ->>> f = FavoriteForm(auto_id=False, label_suffix='') ->>> print f.as_ul() -
      • Favorite color?
      • -
      • Favorite animal
      • ->>> f = FavoriteForm(auto_id=False, label_suffix=u'\u2192') ->>> f.as_ul() -u'
      • Favorite color?
      • \n
      • Favorite animal\u2192
      • ' - -""" + \ -r""" # [This concatenation is to keep the string below the jython's 32K limit]. - -# Initial data ################################################################ - -You can specify initial data for a field by using the 'initial' argument to a -Field class. This initial data is displayed when a Form is rendered with *no* -data. It is not displayed when a Form is rendered with any data (including an -empty dictionary). Also, the initial value is *not* used if data for a -particular required field isn't provided. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, initial='django') -... password = CharField(widget=PasswordInput) - -Here, we're not submitting any data, so the initial value will be displayed. ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • - -Here, we're submitting data, so the initial value will *not* be displayed. ->>> p = UserRegistration({}, auto_id=False) ->>> print p.as_ul() -
        • This field is required.
        Username:
      • -
        • This field is required.
        Password:
      • ->>> p = UserRegistration({'username': u''}, auto_id=False) ->>> print p.as_ul() -
        • This field is required.
        Username:
      • -
        • This field is required.
        Password:
      • ->>> p = UserRegistration({'username': u'foo'}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
        • This field is required.
        Password:
      • - -An 'initial' value is *not* used as a fallback if data is not provided. In this -example, we don't provide a value for 'username', and the form raises a -validation error rather than using the initial value for 'username'. ->>> p = UserRegistration({'password': 'secret'}) ->>> p.errors['username'] -[u'This field is required.'] ->>> p.is_valid() -False - -# Dynamic initial data ######################################################## - -The previous technique dealt with "hard-coded" initial data, but it's also -possible to specify initial data after you've already created the Form class -(i.e., at runtime). Use the 'initial' parameter to the Form constructor. This -should be a dictionary containing initial values for one or more fields in the -form, keyed by field name. - ->>> class UserRegistration(Form): -... username = CharField(max_length=10) -... password = CharField(widget=PasswordInput) - -Here, we're not submitting any data, so the initial value will be displayed. ->>> p = UserRegistration(initial={'username': 'django'}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • ->>> p = UserRegistration(initial={'username': 'stephane'}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • - -The 'initial' parameter is meaningless if you pass data. ->>> p = UserRegistration({}, initial={'username': 'django'}, auto_id=False) ->>> print p.as_ul() -
        • This field is required.
        Username:
      • -
        • This field is required.
        Password:
      • ->>> p = UserRegistration({'username': u''}, initial={'username': 'django'}, auto_id=False) ->>> print p.as_ul() -
        • This field is required.
        Username:
      • -
        • This field is required.
        Password:
      • ->>> p = UserRegistration({'username': u'foo'}, initial={'username': 'django'}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
        • This field is required.
        Password:
      • - -A dynamic 'initial' value is *not* used as a fallback if data is not provided. -In this example, we don't provide a value for 'username', and the form raises a -validation error rather than using the initial value for 'username'. ->>> p = UserRegistration({'password': 'secret'}, initial={'username': 'django'}) ->>> p.errors['username'] -[u'This field is required.'] ->>> p.is_valid() -False - -If a Form defines 'initial' *and* 'initial' is passed as a parameter to Form(), -then the latter will get precedence. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, initial='django') -... password = CharField(widget=PasswordInput) ->>> p = UserRegistration(initial={'username': 'babik'}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • - -# Callable initial data ######################################################## - -The previous technique dealt with raw values as initial data, but it's also -possible to specify callable data. - ->>> class UserRegistration(Form): -... username = CharField(max_length=10) -... password = CharField(widget=PasswordInput) -... options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')]) - -We need to define functions that get called later. ->>> def initial_django(): -... return 'django' ->>> def initial_stephane(): -... return 'stephane' ->>> def initial_options(): -... return ['f','b'] ->>> def initial_other_options(): -... return ['b','w'] - - -Here, we're not submitting any data, so the initial value will be displayed. ->>> p = UserRegistration(initial={'username': initial_django, 'options': initial_options}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • -
      • Options:
      • - -The 'initial' parameter is meaningless if you pass data. ->>> p = UserRegistration({}, initial={'username': initial_django, 'options': initial_options}, auto_id=False) ->>> print p.as_ul() -
        • This field is required.
        Username:
      • -
        • This field is required.
        Password:
      • -
        • This field is required.
        Options:
      • ->>> p = UserRegistration({'username': u''}, initial={'username': initial_django}, auto_id=False) ->>> print p.as_ul() -
        • This field is required.
        Username:
      • -
        • This field is required.
        Password:
      • -
        • This field is required.
        Options:
      • ->>> p = UserRegistration({'username': u'foo', 'options':['f','b']}, initial={'username': initial_django}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
        • This field is required.
        Password:
      • -
      • Options:
      • - -A callable 'initial' value is *not* used as a fallback if data is not provided. -In this example, we don't provide a value for 'username', and the form raises a -validation error rather than using the initial value for 'username'. ->>> p = UserRegistration({'password': 'secret'}, initial={'username': initial_django, 'options': initial_options}) ->>> p.errors['username'] -[u'This field is required.'] ->>> p.is_valid() -False - -If a Form defines 'initial' *and* 'initial' is passed as a parameter to Form(), -then the latter will get precedence. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, initial=initial_django) -... password = CharField(widget=PasswordInput) -... options = MultipleChoiceField(choices=[('f','foo'),('b','bar'),('w','whiz')], initial=initial_other_options) - ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • -
      • Options:
      • ->>> p = UserRegistration(initial={'username': initial_stephane, 'options': initial_options}, auto_id=False) ->>> print p.as_ul() -
      • Username:
      • -
      • Password:
      • -
      • Options:
      • - -# Help text ################################################################### - -You can specify descriptive text for a field by using the 'help_text' argument -to a Field class. This help text is displayed when a Form is rendered. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, help_text='e.g., user@example.com') -... password = CharField(widget=PasswordInput, help_text='Choose wisely.') ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Username: e.g., user@example.com
      • -
      • Password: Choose wisely.
      • ->>> print p.as_p() -

        Username: e.g., user@example.com

        -

        Password: Choose wisely.

        ->>> print p.as_table() -
        - - -The help text is displayed whether or not data is provided for the form. ->>> p = UserRegistration({'username': u'foo'}, auto_id=False) ->>> print p.as_ul() -
      • Username: e.g., user@example.com
      • -
        • This field is required.
        Password: Choose wisely.
      • - -help_text is not displayed for hidden fields. It can be used for documentation -purposes, though. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, help_text='e.g., user@example.com') -... password = CharField(widget=PasswordInput) -... next = CharField(widget=HiddenInput, initial='/', help_text='Redirect destination') ->>> p = UserRegistration(auto_id=False) ->>> print p.as_ul() -
      • Username: e.g., user@example.com
      • -
      • Password:
      • - -Help text can include arbitrary Unicode characters. ->>> class UserRegistration(Form): -... username = CharField(max_length=10, help_text='ŠĐĆŽćžšđ') ->>> p = UserRegistration(auto_id=False) ->>> p.as_ul() -u'
      • Username: \u0160\u0110\u0106\u017d\u0107\u017e\u0161\u0111
      • ' - -# Subclassing forms ########################################################### - -You can subclass a Form to add fields. The resulting form subclass will have -all of the fields of the parent Form, plus whichever fields you define in the -subclass. ->>> class Person(Form): -... first_name = CharField() -... last_name = CharField() -... birthday = DateField() ->>> class Musician(Person): -... instrument = CharField() ->>> p = Person(auto_id=False) ->>> print p.as_ul() -
      • First name:
      • -
      • Last name:
      • -
      • Birthday:
      • ->>> m = Musician(auto_id=False) ->>> print m.as_ul() -
      • First name:
      • -
      • Last name:
      • -
      • Birthday:
      • -
      • Instrument:
      • - -Yes, you can subclass multiple forms. The fields are added in the order in -which the parent classes are listed. ->>> class Person(Form): -... first_name = CharField() -... last_name = CharField() -... birthday = DateField() ->>> class Instrument(Form): -... instrument = CharField() ->>> class Beatle(Person, Instrument): -... haircut_type = CharField() ->>> b = Beatle(auto_id=False) ->>> print b.as_ul() -
      • First name:
      • -
      • Last name:
      • -
      • Birthday:
      • -
      • Instrument:
      • -
      • Haircut type:
      • - -# Forms with prefixes ######################################################### - -Sometimes it's necessary to have multiple forms display on the same HTML page, -or multiple copies of the same form. We can accomplish this with form prefixes. -Pass the keyword argument 'prefix' to the Form constructor to use this feature. -This value will be prepended to each HTML form field name. One way to think -about this is "namespaces for HTML forms". Notice that in the data argument, -each field's key has the prefix, in this case 'person1', prepended to the -actual field name. ->>> class Person(Form): -... first_name = CharField() -... last_name = CharField() -... birthday = DateField() ->>> data = { -... 'person1-first_name': u'John', -... 'person1-last_name': u'Lennon', -... 'person1-birthday': u'1940-10-9' -... } ->>> p = Person(data, prefix='person1') ->>> print p.as_ul() -
      • -
      • -
      • ->>> print p['first_name'] - ->>> print p['last_name'] - ->>> print p['birthday'] - ->>> p.errors -{} ->>> p.is_valid() -True ->>> p.cleaned_data['first_name'] -u'John' ->>> p.cleaned_data['last_name'] -u'Lennon' ->>> p.cleaned_data['birthday'] -datetime.date(1940, 10, 9) - -Let's try submitting some bad data to make sure form.errors and field.errors -work as expected. ->>> data = { -... 'person1-first_name': u'', -... 'person1-last_name': u'', -... 'person1-birthday': u'' -... } ->>> p = Person(data, prefix='person1') ->>> p.errors['first_name'] -[u'This field is required.'] ->>> p.errors['last_name'] -[u'This field is required.'] ->>> p.errors['birthday'] -[u'This field is required.'] ->>> p['first_name'].errors -[u'This field is required.'] ->>> p['person1-first_name'].errors -Traceback (most recent call last): -... -KeyError: "Key 'person1-first_name' not found in Form" - -In this example, the data doesn't have a prefix, but the form requires it, so -the form doesn't "see" the fields. ->>> data = { -... 'first_name': u'John', -... 'last_name': u'Lennon', -... 'birthday': u'1940-10-9' -... } ->>> p = Person(data, prefix='person1') ->>> p.errors['first_name'] -[u'This field is required.'] ->>> p.errors['last_name'] -[u'This field is required.'] ->>> p.errors['birthday'] -[u'This field is required.'] - -With prefixes, a single data dictionary can hold data for multiple instances -of the same form. ->>> data = { -... 'person1-first_name': u'John', -... 'person1-last_name': u'Lennon', -... 'person1-birthday': u'1940-10-9', -... 'person2-first_name': u'Jim', -... 'person2-last_name': u'Morrison', -... 'person2-birthday': u'1943-12-8' -... } ->>> p1 = Person(data, prefix='person1') ->>> p1.is_valid() -True ->>> p1.cleaned_data['first_name'] -u'John' ->>> p1.cleaned_data['last_name'] -u'Lennon' ->>> p1.cleaned_data['birthday'] -datetime.date(1940, 10, 9) ->>> p2 = Person(data, prefix='person2') ->>> p2.is_valid() -True ->>> p2.cleaned_data['first_name'] -u'Jim' ->>> p2.cleaned_data['last_name'] -u'Morrison' ->>> p2.cleaned_data['birthday'] -datetime.date(1943, 12, 8) - -By default, forms append a hyphen between the prefix and the field name, but a -form can alter that behavior by implementing the add_prefix() method. This -method takes a field name and returns the prefixed field, according to -self.prefix. ->>> class Person(Form): -... first_name = CharField() -... last_name = CharField() -... birthday = DateField() -... def add_prefix(self, field_name): -... return self.prefix and '%s-prefix-%s' % (self.prefix, field_name) or field_name ->>> p = Person(prefix='foo') ->>> print p.as_ul() -
      • -
      • -
      • ->>> data = { -... 'foo-prefix-first_name': u'John', -... 'foo-prefix-last_name': u'Lennon', -... 'foo-prefix-birthday': u'1940-10-9' -... } ->>> p = Person(data, prefix='foo') ->>> p.is_valid() -True ->>> p.cleaned_data['first_name'] -u'John' ->>> p.cleaned_data['last_name'] -u'Lennon' ->>> p.cleaned_data['birthday'] -datetime.date(1940, 10, 9) - -# Forms with NullBooleanFields ################################################ - -NullBooleanField is a bit of a special case because its presentation (widget) -is different than its data. This is handled transparently, though. - ->>> class Person(Form): -... name = CharField() -... is_cool = NullBooleanField() ->>> p = Person({'name': u'Joe'}, auto_id=False) ->>> print p['is_cool'] - ->>> p = Person({'name': u'Joe', 'is_cool': u'1'}, auto_id=False) ->>> print p['is_cool'] - ->>> p = Person({'name': u'Joe', 'is_cool': u'2'}, auto_id=False) ->>> print p['is_cool'] - ->>> p = Person({'name': u'Joe', 'is_cool': u'3'}, auto_id=False) ->>> print p['is_cool'] - ->>> p = Person({'name': u'Joe', 'is_cool': True}, auto_id=False) ->>> print p['is_cool'] - ->>> p = Person({'name': u'Joe', 'is_cool': False}, auto_id=False) ->>> print p['is_cool'] - - -# Forms with FileFields ################################################ - -FileFields are a special case because they take their data from the request.FILES, -not request.POST. - ->>> class FileForm(Form): -... file1 = FileField() ->>> f = FileForm(auto_id=False) ->>> print f -
        - ->>> f = FileForm(data={}, files={}, auto_id=False) ->>> print f - - ->>> f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', '')}, auto_id=False) ->>> print f - - ->>> f = FileForm(data={}, files={'file1': 'something that is not a file'}, auto_id=False) ->>> print f - - ->>> f = FileForm(data={}, files={'file1': SimpleUploadedFile('name', 'some content')}, auto_id=False) ->>> print f - ->>> f.is_valid() -True - ->>> f = FileForm(data={}, files={'file1': SimpleUploadedFile('我隻氣墊船裝滿晒鱔.txt', 'मेरी मँडराने वाली नाव सर्पमीनों से भरी ह')}, auto_id=False) ->>> print f - - -# Basic form processing in a view ############################################# - ->>> from django.template import Template, Context ->>> class UserRegistration(Form): -... username = CharField(max_length=10) -... password1 = CharField(widget=PasswordInput) -... password2 = CharField(widget=PasswordInput) -... def clean(self): -... if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: -... raise ValidationError(u'Please make sure your passwords match.') -... return self.cleaned_data ->>> def my_function(method, post_data): -... if method == 'POST': -... form = UserRegistration(post_data, auto_id=False) -... else: -... form = UserRegistration(auto_id=False) -... if form.is_valid(): -... return 'VALID: %r' % form.cleaned_data -... t = Template('
        \n
        {{ result.form.non_field_errors }}
        {{ inline_admin_form.form.non_field_errors }}
        {{ inline_admin_form.form.non_field_errors }}
        %s
        Status:
        Categories:
        Hold down "Control", or "Command" on a Mac, to select more than one.
        name(None)
        %s element.') + row_html = '
        nameParent object
        %s element.') + # make sure that hidden fields are in the correct place + hiddenfields_div = '
        ' % new_child.id + self.assertFalse(table_output.find(hiddenfields_div) == -1, + 'Failed to find hidden fields in: %s' % table_output) + # make sure that list editable fields are rendered in divs correctly + editable_name_field = '' + self.assertFalse('
        %s
        • The two titles must be the same
        • Food delivery with this Driver and Restaurant already exists.
        • Food delivery with this Driver and Restaurant already exists.
        %d%d%d%d
        • This field is required.
        • This field is required.
        • This field is required.
        • This field is required.
        • This field is required.
        • This field is required.
        Name:
        Language:
          -
        • -
        • -
          -
        • -
        • -
        <em>Special</em> Field:
        • Something's wrong with 'Nothing to escape'
        Special Field:
        • 'Nothing to escape' is a safe string
        <em>Special</em> Field:
        • Something's wrong with 'Should escape < & > and <script>alert('xss')</script>'
        Special Field:
        • 'Do not escape' is a safe string
        Username:
        • This field is required.
        Password1:
        • This field is required.
        Password2:
        • This field is required.
        • Please make sure your passwords match.
        Username:
        Password1:
        Password2:
        First name:
        Last name:
        Birthday:
        Field1:
        Field2:
        Field3:
        Field4:
        Default field 1:
        Default field 2:
        Field1:
        Field2:
        Default field 1:
        Default field 2:
        Field3:
        Field4:
        First name:
        Last name:
        Birthday:
        • (Hidden field hidden_text) This field is required.
        First name:
        Last name:
        Birthday:
        Field1:
        Field2:
        Field3:
        Field4:
        Field5:
        Field6:
        Field7:
        Field8:
        Field9:
        Field10:
        Field11:
        Field12:
        Field13:
        Field14:
        Username:
        e.g., user@example.com
        Password:
        Choose wisely.
        File1:
        File1:
        • This field is required.
        File1:
        • The submitted file is empty.
        File1:
        • No file was submitted. Check the encoding type on the form.
        File1:
        File1:
        \n{{ form }}\n
        \n\n') -... return t.render(Context({'form': form})) - -Case 1: GET (an empty form, with no errors). ->>> print my_function('GET', {}) -
        - - - - -
        Username:
        Password1:
        Password2:
        - -
        - -Case 2: POST with erroneous data (a redisplayed form, with errors). ->>> print my_function('POST', {'username': 'this-is-a-long-username', 'password1': 'foo', 'password2': 'bar'}) -
        - - - - - -
        • Please make sure your passwords match.
        Username:
        • Ensure this value has at most 10 characters (it has 23).
        Password1:
        Password2:
        - -
        - -Case 3: POST with valid data (the success message). ->>> print my_function('POST', {'username': 'adrian', 'password1': 'secret', 'password2': 'secret'}) -VALID: {'username': u'adrian', 'password1': u'secret', 'password2': u'secret'} - -# Some ideas for using templates with forms ################################### - ->>> class UserRegistration(Form): -... username = CharField(max_length=10, help_text="Good luck picking a username that doesn't already exist.") -... password1 = CharField(widget=PasswordInput) -... password2 = CharField(widget=PasswordInput) -... def clean(self): -... if self.cleaned_data.get('password1') and self.cleaned_data.get('password2') and self.cleaned_data['password1'] != self.cleaned_data['password2']: -... raise ValidationError(u'Please make sure your passwords match.') -... return self.cleaned_data - -You have full flexibility in displaying form fields in a template. Just pass a -Form instance to the template, and use "dot" access to refer to individual -fields. Note, however, that this flexibility comes with the responsibility of -displaying all the errors, including any that might not be associated with a -particular field. ->>> t = Template('''
        -... {{ form.username.errors.as_ul }}

        -... {{ form.password1.errors.as_ul }}

        -... {{ form.password2.errors.as_ul }}

        -... -...
        ''') ->>> print t.render(Context({'form': UserRegistration(auto_id=False)})) -
        -

        -

        -

        - -
        ->>> print t.render(Context({'form': UserRegistration({'username': 'django'}, auto_id=False)})) -
        -

        -
        • This field is required.

        -
        • This field is required.

        - -
        - -Use form.[field].label to output a field's label. You can specify the label for -a field by using the 'label' argument to a Field class. If you don't specify -'label', Django will use the field name with underscores converted to spaces, -and the initial letter capitalized. ->>> t = Template('''
        -...

        -...

        -...

        -... -...
        ''') ->>> print t.render(Context({'form': UserRegistration(auto_id=False)})) -
        -

        -

        -

        - -
        - -User form.[field].label_tag to output a field's label with a