diff --git a/AUTHORS b/AUTHORS index 973e32d05a..80f277996b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,18 +12,25 @@ The PRIMARY AUTHORS are (and/or have been): * Luke Plant * Russell Keith-Magee * Robert Wittams + * James Bennett * Gary Wilson + * Matt Boersma + * Ian Kelly + * Joseph Kocherhans * Brian Rosner * Justin Bronn * Karen Tracey * Jannis Leidel * James Tauber * Alex Gaynor + * Simon Meers * Andrew Godwin * Carl Meyer * Ramiro Morales + * Gabriel Hurley * Chris Beaven * Honza Král + * Tim Graham * Idan Gazit * Paul McMillan * Julien Phalip @@ -36,6 +43,7 @@ The PRIMARY AUTHORS are (and/or have been): * Preston Holmes * Simon Charette * Donald Stufft + * Daniel Lindsley * Marc Tamlyn More information on the main contributors to Django can be found in @@ -84,14 +92,15 @@ answer newbie questions, and generally made Django that much better: Randy Barlow Scott Barr Jiri Barton + Jorge Bastida Ned Batchelder batiste@dosimple.ch Batman Brian Beck Shannon -jj Behrens Esdras Beleza + Božidar Benko Chris Bennett - James Bennett Danilo Bargen Shai Berger berto @@ -102,9 +111,9 @@ answer newbie questions, and generally made Django that much better: Paul Bissex Loïc Bistuer Simon Blanchard + Jérémie Blaser Craig Blaszczyk David Blewett - Matt Boersma Artem Gnilov Matías Bordese Nate Bragg @@ -117,6 +126,7 @@ answer newbie questions, and generally made Django that much better: bthomas btoll@bestweb.net Jonathan Buchanan + Jacob Burch Keith Bussell C8E Chris Cahoon @@ -149,6 +159,7 @@ answer newbie questions, and generally made Django that much better: Paul Collins Robert Coup Deric Crago + Brian Fabian Crain David Cramer Pete Crosier Matt Croydon @@ -156,6 +167,7 @@ answer newbie questions, and generally made Django that much better: Leah Culver Raúl Cumplido flavio.curella@gmail.com + Tome Cvitan John D'Agostino dackze+django@gmail.com Jim Dalton @@ -188,6 +200,7 @@ answer newbie questions, and generally made Django that much better: J. Clifford Dyer Clint Ecker Nick Efford + Marc Egli eibaan@gmail.com David Eklund Julia Elman @@ -212,6 +225,7 @@ answer newbie questions, and generally made Django that much better: Stefane Fermgier J. Pablo Fernandez Maciej Fijalkowski + Leandra Finger Juan Pedro Fisanotti Ben Firshman Matthew Flanagan @@ -239,6 +253,7 @@ answer newbie questions, and generally made Django that much better: pradeep.gowda@gmail.com Collin Grady Gabriel Grant + Martin Green Daniel Greenfeld Simon Greenhill Owen Griffiths @@ -268,6 +283,7 @@ answer newbie questions, and generally made Django that much better: Eric Holscher Ian Holsman Kieran Holland + Markus Holtermann Sung-Jin Hong Leo "hylje" Honkanen Matt Hoskins @@ -278,7 +294,6 @@ answer newbie questions, and generally made Django that much better: Rob Hudson Jason Huggins Jeff Hui - Gabriel Hurley Hyun Mi Ae Ibon Tom Insam @@ -327,12 +342,12 @@ answer newbie questions, and generally made Django that much better: Meir Kriheli Bruce Kroeze krzysiek.pawlik@silvermedia.pl - Joseph Kocherhans konrad@gwu.edu knox David Krauth Kevin Kubasik kurtiss@meetro.com + Vladimir Kuzma Denis Kuzmichyov Panos Laganakos Nick Lane @@ -360,7 +375,6 @@ answer newbie questions, and generally made Django that much better: limodou Philip Lindborg Simon Litchfield - Daniel Lindsley Trey Long Laurent Luce Martin Mahner @@ -399,6 +413,7 @@ answer newbie questions, and generally made Django that much better: Slawek Mikula Katie Miller Shawn Milochik + Baptiste Mispelon mitakummaa@gmail.com Taylor Mitchell mmarshall @@ -458,6 +473,7 @@ answer newbie questions, and generally made Django that much better: Jyrki Pulliainen Thejaswi Puthraya Johann Queuniet + Ram Rachum Jan Rademaker Michael Radziej Laurent Rahuel @@ -465,6 +481,7 @@ answer newbie questions, and generally made Django that much better: Luciano Ramalho Amit Ramon Philippe Raoult + Senko Rašić Massimiliano Ravelli Brian Ray Lee Reilly @@ -480,6 +497,7 @@ answer newbie questions, and generally made Django that much better: Alex Robbins Matt Robenolt Henrique Romano + Erik Romijn Armin Ronacher Daniel Roseman Rozza @@ -499,6 +517,7 @@ answer newbie questions, and generally made Django that much better: Bernd Schlapsi schwank@gmail.com scott@staplefish.com + Olivier Sels Ilya Semenov Aleksandra Sendecka serbaut@gmail.com @@ -523,11 +542,13 @@ answer newbie questions, and generally made Django that much better: Don Spaulding Calvin Spealman Dane Springmeyer + Silvan Spross Bjørn Stabell Georgi Stanojevski starrynight Vasiliy Stavenko Thomas Steinacher + Emil Stenström Johan C. Stöver Nowell Strite Thomas Stromberg @@ -573,12 +594,14 @@ answer newbie questions, and generally made Django that much better: I.S. van Oostveen viestards.lists@gmail.com George Vilches + Simeon Visser Vlado Zachary Voase Marijn Vriens Milton Waddams Chris Wagner Rick Wagner + Gavin Wahl wam-djangobug@wamber.net Wang Chun Filip Wasilewski diff --git a/django/__init__.py b/django/__init__.py index 873c328add..5a1c74efa7 100644 --- a/django/__init__.py +++ b/django/__init__.py @@ -1,4 +1,4 @@ -VERSION = (1, 6, 0, 'alpha', 0) +VERSION = (1, 6, 0, 'alpha', 1) def get_version(*args, **kwargs): # Don't litter django/__init__.py with all the get_version stuff. diff --git a/django/conf/__init__.py b/django/conf/__init__.py index f876c490c8..61584391cd 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -127,7 +127,10 @@ class Settings(BaseSettings): try: mod = importlib.import_module(self.SETTINGS_MODULE) except ImportError as e: - raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_MODULE, e)) + raise ImportError( + "Could not import settings '%s' (Is it on sys.path? Is there an import error in the settings file?): %s" + % (self.SETTINGS_MODULE, e) + ) # Settings that should be converted into tuples if they're mistakenly entered # as strings. diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 53aef351c0..596f4ae78a 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -131,7 +131,7 @@ LANGUAGES = ( ) # Languages using BiDi (right-to-left) layout -LANGUAGES_BIDI = ("he", "ar", "fa") +LANGUAGES_BIDI = ("he", "ar", "fa", "ur") # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. diff --git a/django/conf/locale/en/LC_MESSAGES/django.po b/django/conf/locale/en/LC_MESSAGES/django.po index 4c94d5a00a..371f0af2ab 100644 --- a/django/conf/locale/en/LC_MESSAGES/django.po +++ b/django/conf/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:17+0200\n" +"POT-Creation-Date: 2013-05-25 14:27+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -337,7 +337,7 @@ msgstr "" msgid "Enter a valid value." msgstr "" -#: core/validators.py:53 forms/fields.py:640 +#: core/validators.py:53 forms/fields.py:639 msgid "Enter a valid URL." msgstr "" @@ -362,7 +362,7 @@ msgstr "" msgid "Enter a valid IPv4 or IPv6 address." msgstr "" -#: core/validators.py:175 db/models/fields/__init__.py:704 +#: core/validators.py:175 db/models/fields/__init__.py:706 msgid "Enter only digits separated by commas." msgstr "" @@ -408,7 +408,7 @@ msgstr[1] "" msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s." msgstr "" -#: db/models/base.py:905 forms/models.py:605 +#: db/models/base.py:905 forms/models.py:643 msgid "and" msgstr "" @@ -435,156 +435,156 @@ msgstr "" msgid "Field of type: %(field_type)s" msgstr "" -#: db/models/fields/__init__.py:568 db/models/fields/__init__.py:1034 +#: db/models/fields/__init__.py:570 db/models/fields/__init__.py:1036 msgid "Integer" msgstr "" -#: db/models/fields/__init__.py:572 db/models/fields/__init__.py:1032 +#: db/models/fields/__init__.py:574 db/models/fields/__init__.py:1034 #, python-format msgid "'%s' value must be an integer." msgstr "" -#: db/models/fields/__init__.py:620 +#: db/models/fields/__init__.py:622 #, python-format msgid "'%s' value must be either True or False." msgstr "" -#: db/models/fields/__init__.py:622 +#: db/models/fields/__init__.py:624 msgid "Boolean (Either True or False)" msgstr "" -#: db/models/fields/__init__.py:671 +#: db/models/fields/__init__.py:673 #, python-format msgid "String (up to %(max_length)s)" msgstr "" -#: db/models/fields/__init__.py:699 +#: db/models/fields/__init__.py:701 msgid "Comma-separated integers" msgstr "" -#: db/models/fields/__init__.py:713 +#: db/models/fields/__init__.py:715 #, python-format msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format." msgstr "" -#: db/models/fields/__init__.py:715 db/models/fields/__init__.py:803 +#: db/models/fields/__init__.py:717 db/models/fields/__init__.py:805 #, python-format msgid "" "'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date." msgstr "" -#: db/models/fields/__init__.py:718 +#: db/models/fields/__init__.py:720 msgid "Date (without time)" msgstr "" -#: db/models/fields/__init__.py:801 +#: db/models/fields/__init__.py:803 #, python-format msgid "" "'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." "uuuuuu]][TZ] format." msgstr "" -#: db/models/fields/__init__.py:805 +#: db/models/fields/__init__.py:807 #, python-format msgid "" "'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but " "it is an invalid date/time." msgstr "" -#: db/models/fields/__init__.py:809 +#: db/models/fields/__init__.py:811 msgid "Date (with time)" msgstr "" -#: db/models/fields/__init__.py:898 +#: db/models/fields/__init__.py:900 #, python-format msgid "'%s' value must be a decimal number." msgstr "" -#: db/models/fields/__init__.py:900 +#: db/models/fields/__init__.py:902 msgid "Decimal number" msgstr "" -#: db/models/fields/__init__.py:957 +#: db/models/fields/__init__.py:959 msgid "Email address" msgstr "" -#: db/models/fields/__init__.py:976 +#: db/models/fields/__init__.py:978 msgid "File path" msgstr "" -#: db/models/fields/__init__.py:1003 +#: db/models/fields/__init__.py:1005 #, python-format msgid "'%s' value must be a float." msgstr "" -#: db/models/fields/__init__.py:1005 +#: db/models/fields/__init__.py:1007 msgid "Floating point number" msgstr "" -#: db/models/fields/__init__.py:1066 +#: db/models/fields/__init__.py:1068 msgid "Big (8 byte) integer" msgstr "" -#: db/models/fields/__init__.py:1080 +#: db/models/fields/__init__.py:1082 msgid "IPv4 address" msgstr "" -#: db/models/fields/__init__.py:1096 +#: db/models/fields/__init__.py:1098 msgid "IP address" msgstr "" -#: db/models/fields/__init__.py:1139 +#: db/models/fields/__init__.py:1141 #, python-format msgid "'%s' value must be either None, True or False." msgstr "" -#: db/models/fields/__init__.py:1141 +#: db/models/fields/__init__.py:1143 msgid "Boolean (Either True, False or None)" msgstr "" -#: db/models/fields/__init__.py:1190 +#: db/models/fields/__init__.py:1192 msgid "Positive integer" msgstr "" -#: db/models/fields/__init__.py:1201 +#: db/models/fields/__init__.py:1203 msgid "Positive small integer" msgstr "" -#: db/models/fields/__init__.py:1212 +#: db/models/fields/__init__.py:1214 #, python-format msgid "Slug (up to %(max_length)s)" msgstr "" -#: db/models/fields/__init__.py:1230 +#: db/models/fields/__init__.py:1232 msgid "Small integer" msgstr "" -#: db/models/fields/__init__.py:1236 +#: db/models/fields/__init__.py:1238 msgid "Text" msgstr "" -#: db/models/fields/__init__.py:1254 +#: db/models/fields/__init__.py:1256 #, python-format msgid "" "'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format." msgstr "" -#: db/models/fields/__init__.py:1256 +#: db/models/fields/__init__.py:1258 #, python-format msgid "" "'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid " "time." msgstr "" -#: db/models/fields/__init__.py:1259 +#: db/models/fields/__init__.py:1261 msgid "Time" msgstr "" -#: db/models/fields/__init__.py:1321 +#: db/models/fields/__init__.py:1323 msgid "URL" msgstr "" -#: db/models/fields/__init__.py:1338 +#: db/models/fields/__init__.py:1340 msgid "Raw binary data" msgstr "" @@ -596,55 +596,50 @@ msgstr "" msgid "Image" msgstr "" -#: db/models/fields/related.py:1133 +#: db/models/fields/related.py:1118 #, python-format msgid "Model %(model)s with pk %(pk)r does not exist." msgstr "" -#: db/models/fields/related.py:1135 +#: db/models/fields/related.py:1120 msgid "Foreign Key (type determined by related field)" msgstr "" -#: db/models/fields/related.py:1272 +#: db/models/fields/related.py:1257 msgid "One-to-one relationship" msgstr "" -#: db/models/fields/related.py:1339 +#: db/models/fields/related.py:1324 msgid "Many-to-many relationship" msgstr "" -#: db/models/fields/related.py:1366 -msgid "" -"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." -msgstr "" - #: forms/fields.py:56 msgid "This field is required." msgstr "" -#: forms/fields.py:225 +#: forms/fields.py:227 msgid "Enter a whole number." msgstr "" -#: forms/fields.py:266 forms/fields.py:294 +#: forms/fields.py:268 forms/fields.py:296 msgid "Enter a number." msgstr "" -#: forms/fields.py:296 +#: forms/fields.py:298 #, python-format msgid "Ensure that there are no more than %(max)s digit in total." msgid_plural "Ensure that there are no more than %(max)s digits in total." msgstr[0] "" msgstr[1] "" -#: forms/fields.py:300 +#: forms/fields.py:302 #, python-format msgid "Ensure that there are no more than %(max)s decimal place." msgid_plural "Ensure that there are no more than %(max)s decimal places." msgstr[0] "" msgstr[1] "" -#: forms/fields.py:304 +#: forms/fields.py:306 #, python-format msgid "" "Ensure that there are no more than %(max)s digit before the decimal point." @@ -653,31 +648,31 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: forms/fields.py:406 forms/fields.py:1058 +#: forms/fields.py:408 forms/fields.py:1064 msgid "Enter a valid date." msgstr "" -#: forms/fields.py:430 forms/fields.py:1059 +#: forms/fields.py:432 forms/fields.py:1065 msgid "Enter a valid time." msgstr "" -#: forms/fields.py:451 +#: forms/fields.py:454 msgid "Enter a valid date/time." msgstr "" -#: forms/fields.py:525 +#: forms/fields.py:531 msgid "No file was submitted. Check the encoding type on the form." msgstr "" -#: forms/fields.py:526 +#: forms/fields.py:532 msgid "No file was submitted." msgstr "" -#: forms/fields.py:527 +#: forms/fields.py:533 msgid "The submitted file is empty." msgstr "" -#: forms/fields.py:529 +#: forms/fields.py:535 #, python-format msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." msgid_plural "" @@ -685,22 +680,22 @@ msgid_plural "" msgstr[0] "" msgstr[1] "" -#: forms/fields.py:532 +#: forms/fields.py:538 msgid "Please either submit a file or check the clear checkbox, not both." msgstr "" -#: forms/fields.py:593 +#: forms/fields.py:599 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: forms/fields.py:746 forms/fields.py:824 forms/models.py:1042 +#: forms/fields.py:749 forms/fields.py:828 forms/models.py:1096 #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" -#: forms/fields.py:825 forms/fields.py:928 forms/models.py:1041 +#: forms/fields.py:829 forms/fields.py:933 forms/models.py:1095 msgid "Enter a list of values." msgstr "" @@ -709,53 +704,60 @@ msgstr "" msgid "(Hidden field %(name)s) %(error)s" msgstr "" -#: forms/formsets.py:305 +#: forms/formsets.py:310 #, python-format -msgid "Please submit %s or fewer forms." -msgstr "" +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" +msgstr[1] "" -#: forms/formsets.py:331 forms/formsets.py:333 +#: forms/formsets.py:337 forms/formsets.py:339 msgid "Order" msgstr "" -#: forms/formsets.py:335 +#: forms/formsets.py:341 msgid "Delete" msgstr "" -#: forms/models.py:599 +#: forms/models.py:637 #, python-format msgid "Please correct the duplicate data for %(field)s." msgstr "" -#: forms/models.py:603 +#: forms/models.py:641 #, python-format msgid "Please correct the duplicate data for %(field)s, which must be unique." msgstr "" -#: forms/models.py:609 +#: forms/models.py:647 #, python-format msgid "" "Please correct the duplicate data for %(field_name)s which must be unique " "for the %(lookup)s in %(date_field)s." msgstr "" -#: forms/models.py:617 +#: forms/models.py:655 msgid "Please correct the duplicate values below." msgstr "" -#: forms/models.py:883 +#: forms/models.py:937 msgid "The inline foreign key did not match the parent instance primary key." msgstr "" -#: forms/models.py:947 +#: forms/models.py:1001 msgid "Select a valid choice. That choice is not one of the available choices." msgstr "" -#: forms/models.py:1044 +#: forms/models.py:1098 #, python-format msgid "\"%(pk)s\" is not a valid value for a primary key." msgstr "" +#: forms/models.py:1109 +msgid "" +"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "" + #: forms/util.py:84 #, python-format msgid "" @@ -791,34 +793,34 @@ msgstr "" msgid "yes,no,maybe" msgstr "" -#: template/defaultfilters.py:813 template/defaultfilters.py:824 +#: template/defaultfilters.py:813 template/defaultfilters.py:825 #, python-format msgid "%(size)d byte" msgid_plural "%(size)d bytes" msgstr[0] "" msgstr[1] "" -#: template/defaultfilters.py:826 +#: template/defaultfilters.py:827 #, python-format msgid "%s KB" msgstr "" -#: template/defaultfilters.py:828 +#: template/defaultfilters.py:829 #, python-format msgid "%s MB" msgstr "" -#: template/defaultfilters.py:830 +#: template/defaultfilters.py:831 #, python-format msgid "%s GB" msgstr "" -#: template/defaultfilters.py:832 +#: template/defaultfilters.py:833 #, python-format msgid "%s TB" msgstr "" -#: template/defaultfilters.py:833 +#: template/defaultfilters.py:835 #, python-format msgid "%s PB" msgstr "" @@ -1119,6 +1121,16 @@ msgctxt "alt. month" msgid "December" msgstr "" +#: utils/image.py:105 +#, python-format +msgid "Neither Pillow nor PIL could be imported: %s" +msgstr "" + +#: utils/image.py:127 +#, python-format +msgid "The '_imaging' module for the PIL could not be imported: %s" +msgstr "" + #: utils/text.py:70 #, python-format msgctxt "String to return when truncating text" @@ -1130,53 +1142,53 @@ msgid "or" msgstr "" #. Translators: This string is used as a separator between list elements -#: utils/text.py:242 utils/timesince.py:54 +#: utils/text.py:242 utils/timesince.py:55 msgid ", " msgstr "" -#: utils/timesince.py:22 +#: utils/timesince.py:23 #, python-format msgid "%d year" msgid_plural "%d years" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:23 +#: utils/timesince.py:24 #, python-format msgid "%d month" msgid_plural "%d months" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:24 +#: utils/timesince.py:25 #, python-format msgid "%d week" msgid_plural "%d weeks" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:25 +#: utils/timesince.py:26 #, python-format msgid "%d day" msgid_plural "%d days" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:26 +#: utils/timesince.py:27 #, python-format msgid "%d hour" msgid_plural "%d hours" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:27 +#: utils/timesince.py:28 #, python-format msgid "%d minute" msgid_plural "%d minutes" msgstr[0] "" msgstr[1] "" -#: utils/timesince.py:43 +#: utils/timesince.py:44 msgid "0 minutes" msgstr "" diff --git a/django/conf/urls/__init__.py b/django/conf/urls/__init__.py index 04fb1dff59..c0340c0543 100644 --- a/django/conf/urls/__init__.py +++ b/django/conf/urls/__init__.py @@ -5,8 +5,9 @@ from django.utils.importlib import import_module from django.utils import six -__all__ = ['handler403', 'handler404', 'handler500', 'include', 'patterns', 'url'] +__all__ = ['handler400', 'handler403', 'handler404', 'handler500', 'include', 'patterns', 'url'] +handler400 = 'django.views.defaults.bad_request' handler403 = 'django.views.defaults.permission_denied' handler404 = 'django.views.defaults.page_not_found' handler500 = 'django.views.defaults.server_error' diff --git a/django/contrib/admin/exceptions.py b/django/contrib/admin/exceptions.py new file mode 100644 index 0000000000..2e094c6da1 --- /dev/null +++ b/django/contrib/admin/exceptions.py @@ -0,0 +1,6 @@ +from django.core.exceptions import SuspiciousOperation + + +class DisallowedModelAdminLookup(SuspiciousOperation): + """Invalid filter was passed to admin view via URL querystring""" + pass diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py index ff66d3e3f3..4131494515 100644 --- a/django/contrib/admin/filters.py +++ b/django/contrib/admin/filters.py @@ -216,7 +216,7 @@ class RelatedFieldListFilter(FieldListFilter): } FieldListFilter.register(lambda f: ( - hasattr(f, 'rel') and bool(f.rel) or + bool(f.rel) if hasattr(f, 'rel') else isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter) diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py index adc2302587..320d3267a7 100644 --- a/django/contrib/admin/helpers.py +++ b/django/contrib/admin/helpers.py @@ -131,7 +131,7 @@ class AdminField(object): classes.append('required') if not self.is_first: classes.append('inline') - attrs = classes and {'class': ' '.join(classes)} or {} + attrs = {'class': ' '.join(classes)} if classes else {} return self.field.label_tag(contents=mark_safe(contents), attrs=attrs) def errors(self): @@ -144,7 +144,7 @@ class AdminReadonlyField(object): # {{ field.name }} must be a useful class name to identify the field. # For convenience, store other field-related data here too. if callable(field): - class_name = field.__name__ != '' and field.__name__ or '' + class_name = field.__name__ if field.__name__ != '' else '' else: class_name = field self.field = { diff --git a/django/contrib/admin/locale/en/LC_MESSAGES/django.po b/django/contrib/admin/locale/en/LC_MESSAGES/django.po index 964b0a509b..8994d24cbb 100644 --- a/django/contrib/admin/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/admin/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:18+0200\n" +"POT-Creation-Date: 2013-05-25 14:19+0200\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -18,12 +18,12 @@ msgstr "" msgid "Successfully deleted %(count)d %(items)s." msgstr "" -#: actions.py:61 options.py:1365 +#: actions.py:61 options.py:1418 #, python-format msgid "Cannot delete %(name)s" msgstr "" -#: actions.py:63 options.py:1367 +#: actions.py:63 options.py:1420 msgid "Are you sure?" msgstr "" @@ -130,157 +130,157 @@ msgstr "" msgid "LogEntry Object" msgstr "" -#: options.py:163 options.py:192 +#: options.py:173 options.py:202 msgid "None" msgstr "" -#: options.py:710 +#: options.py:763 #, python-format msgid "Changed %s." msgstr "" -#: options.py:710 options.py:720 options.py:1514 +#: options.py:763 options.py:773 options.py:1570 msgid "and" msgstr "" -#: options.py:715 +#: options.py:768 #, python-format msgid "Added %(name)s \"%(object)s\"." msgstr "" -#: options.py:719 +#: options.py:772 #, python-format msgid "Changed %(list)s for %(name)s \"%(object)s\"." msgstr "" -#: options.py:724 +#: options.py:777 #, python-format msgid "Deleted %(name)s \"%(object)s\"." msgstr "" -#: options.py:728 +#: options.py:781 msgid "No fields changed." msgstr "" -#: options.py:831 options.py:874 +#: options.py:884 options.py:927 #, python-format msgid "" "The %(name)s \"%(obj)s\" was added successfully. You may edit it again below." msgstr "" -#: options.py:849 +#: options.py:902 #, python-format msgid "" "The %(name)s \"%(obj)s\" was added successfully. You may add another " "%(name)s below." msgstr "" -#: options.py:853 +#: options.py:906 #, python-format msgid "The %(name)s \"%(obj)s\" was added successfully." msgstr "" -#: options.py:867 +#: options.py:920 #, python-format msgid "" "The %(name)s \"%(obj)s\" was changed successfully. You may edit it again " "below." msgstr "" -#: options.py:881 +#: options.py:934 #, python-format msgid "" "The %(name)s \"%(obj)s\" was changed successfully. You may add another " "%(name)s below." msgstr "" -#: options.py:887 +#: options.py:940 #, python-format msgid "The %(name)s \"%(obj)s\" was changed successfully." msgstr "" -#: options.py:965 options.py:1225 +#: options.py:1018 options.py:1278 msgid "" "Items must be selected in order to perform actions on them. No items have " "been changed." msgstr "" -#: options.py:984 +#: options.py:1037 msgid "No action selected." msgstr "" -#: options.py:1064 +#: options.py:1117 #, python-format msgid "Add %s" msgstr "" -#: options.py:1088 options.py:1333 +#: options.py:1141 options.py:1386 #, python-format msgid "%(name)s object with primary key %(key)r does not exist." msgstr "" -#: options.py:1154 +#: options.py:1207 #, python-format msgid "Change %s" msgstr "" -#: options.py:1204 +#: options.py:1257 msgid "Database error" msgstr "" -#: options.py:1267 +#: options.py:1320 #, python-format msgid "%(count)s %(name)s was changed successfully." msgid_plural "%(count)s %(name)s were changed successfully." msgstr[0] "" msgstr[1] "" -#: options.py:1294 +#: options.py:1347 #, python-format msgid "%(total_count)s selected" msgid_plural "All %(total_count)s selected" msgstr[0] "" msgstr[1] "" -#: options.py:1299 +#: options.py:1352 #, python-format msgid "0 of %(cnt)s selected" msgstr "" -#: options.py:1350 +#: options.py:1403 #, python-format msgid "The %(name)s \"%(obj)s\" was deleted successfully." msgstr "" -#: options.py:1406 +#: options.py:1459 #, python-format msgid "Change history: %s" msgstr "" #. Translators: Model verbose name and instance representation, suitable to be an item in a list -#: options.py:1508 +#: options.py:1564 #, python-format msgid "%(class_name)s %(instance)s" msgstr "" -#: options.py:1515 +#: options.py:1571 #, python-format msgid "" "Deleting %(class_name)s %(instance)s would require deleting the following " "protected related objects: %(related_objects)s" msgstr "" -#: sites.py:324 tests.py:71 templates/admin/login.html:48 +#: sites.py:318 tests.py:71 templates/admin/login.html:48 #: templates/registration/password_reset_complete.html:19 #: views/decorators.py:24 msgid "Log in" msgstr "" -#: sites.py:392 +#: sites.py:386 msgid "Site administration" msgstr "" -#: sites.py:446 +#: sites.py:440 #, python-format msgid "%s administration" msgstr "" @@ -429,9 +429,14 @@ msgstr "" #: templates/admin/auth/user/change_password.html:27 #: templates/registration/password_change_form.html:20 msgid "Please correct the error below." -msgid_plural "Please correct the errors below." -msgstr[0] "" -msgstr[1] "" +msgstr "" + +#: templates/admin/change_form.html:44 templates/admin/change_list.html:67 +#: templates/admin/login.html:17 +#: templates/admin/auth/user/change_password.html:27 +#: templates/registration/password_change_form.html:20 +msgid "Please correct the errors below." +msgstr "" #: templates/admin/change_list.html:58 #, python-format @@ -811,16 +816,16 @@ msgstr "" msgid "All dates" msgstr "" -#: views/main.py:37 +#: views/main.py:35 msgid "(None)" msgstr "" -#: views/main.py:86 +#: views/main.py:84 #, python-format msgid "Select %s" msgstr "" -#: views/main.py:88 +#: views/main.py:86 #, python-format msgid "Select %s to change" msgstr "" diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 7373837bb0..34583ebf74 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -1,5 +1,6 @@ import copy -from functools import update_wrapper, partial +import operator +from functools import partial, reduce, update_wrapper from django import forms from django.conf import settings @@ -9,7 +10,8 @@ from django.forms.models import (modelform_factory, modelformset_factory, from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects, - model_format_dict, NestedObjects) + model_format_dict, NestedObjects, lookup_needs_distinct) +from django.contrib.admin import validation from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect @@ -22,6 +24,7 @@ from django.db.models.related import RelatedObject from django.db.models.fields import BLANK_CHOICE_DASH, FieldDoesNotExist from django.db.models.sql.constants import QUERY_TERMS from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http.response import HttpResponseBase from django.shortcuts import get_object_or_404 from django.template.response import SimpleTemplateResponse, TemplateResponse from django.utils.decorators import method_decorator @@ -87,6 +90,14 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): readonly_fields = () ordering = None + # validation + validator_class = validation.BaseValidator + + @classmethod + def validate(cls, model): + validator = cls.validator_class() + validator.validate(cls, model) + def __init__(self): overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy() overrides.update(self.formfield_overrides) @@ -246,6 +257,34 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): """ return self.prepopulated_fields + def get_search_results(self, request, queryset, search_term): + # Apply keyword searches. + def construct_search(field_name): + if field_name.startswith('^'): + return "%s__istartswith" % field_name[1:] + elif field_name.startswith('='): + return "%s__iexact" % field_name[1:] + elif field_name.startswith('@'): + return "%s__search" % field_name[1:] + else: + return "%s__icontains" % field_name + + use_distinct = False + if self.search_fields and search_term: + orm_lookups = [construct_search(str(search_field)) + for search_field in self.search_fields] + for bit in search_term.split(): + or_queries = [models.Q(**{orm_lookup: bit}) + for orm_lookup in orm_lookups] + queryset = queryset.filter(reduce(operator.or_, or_queries)) + if not use_distinct: + for search_spec in orm_lookups: + if lookup_needs_distinct(self.opts, search_spec): + use_distinct = True + break + + return queryset, use_distinct + def get_queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the @@ -371,6 +410,9 @@ class ModelAdmin(BaseModelAdmin): actions_on_bottom = False actions_selection_counter = True + # validation + validator_class = validation.ModelAdminValidator + def __init__(self, model, admin_site): self.model = model self.opts = model._meta @@ -456,7 +498,7 @@ class ModelAdmin(BaseModelAdmin): "Hook for specifying fieldsets for the add form." if self.declared_fieldsets: return self.declared_fieldsets - form = self.get_form(request, obj) + form = self.get_form(request, obj, fields=None) fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) return [(None, {'fields': fields})] @@ -465,10 +507,10 @@ class ModelAdmin(BaseModelAdmin): Returns a Form class for use in the admin add view. This is used by add_view and change_view. """ - if self.declared_fieldsets: - fields = flatten_fieldsets(self.declared_fieldsets) + if 'fields' in kwargs: + fields = kwargs.pop('fields') else: - fields = None + fields = flatten_fieldsets(self.get_fieldsets(request, obj)) if self.exclude is None: exclude = [] else: @@ -985,10 +1027,10 @@ class ModelAdmin(BaseModelAdmin): response = func(self, request, queryset) - # Actions may return an HttpResponse, which will be used as the - # response from the POST. If not, we'll be a good little HTTP - # citizen and redirect back to the changelist page. - if isinstance(response, HttpResponse): + # Actions may return an HttpResponse-like object, which will be + # used as the response from the POST. If not, we'll be a good + # little HTTP citizen and redirect back to the changelist page. + if isinstance(response, HttpResponseBase): return response else: return HttpResponseRedirect(request.get_full_path()) @@ -1447,6 +1489,9 @@ class InlineModelAdmin(BaseModelAdmin): verbose_name_plural = None can_delete = True + # validation + validator_class = validation.InlineValidator + def __init__(self, parent_model, admin_site): self.admin_site = admin_site self.parent_model = parent_model @@ -1467,12 +1512,20 @@ class InlineModelAdmin(BaseModelAdmin): js.extend(['SelectBox.js', 'SelectFilter2.js']) return forms.Media(js=[static('admin/js/%s' % url) for url in js]) + def get_extra(self, request, obj=None, **kwargs): + """Hook for customizing the number of extra inline forms.""" + return self.extra + + def get_max_num(self, request, obj=None, **kwargs): + """Hook for customizing the max number of extra inline forms.""" + return self.max_num + def get_formset(self, request, obj=None, **kwargs): """Returns a BaseInlineFormSet class for use in admin add/change views.""" - if self.declared_fieldsets: - fields = flatten_fieldsets(self.declared_fieldsets) + if 'fields' in kwargs: + fields = kwargs.pop('fields') else: - fields = None + fields = flatten_fieldsets(self.get_fieldsets(request, obj)) if self.exclude is None: exclude = [] else: @@ -1493,8 +1546,8 @@ class InlineModelAdmin(BaseModelAdmin): "fields": fields, "exclude": exclude, "formfield_callback": partial(self.formfield_for_dbfield, request=request), - "extra": self.extra, - "max_num": self.max_num, + "extra": self.get_extra(request, obj, **kwargs), + "max_num": self.get_max_num(request, obj, **kwargs), "can_delete": can_delete, } @@ -1544,7 +1597,7 @@ class InlineModelAdmin(BaseModelAdmin): def get_fieldsets(self, request, obj=None): if self.declared_fieldsets: return self.declared_fieldsets - form = self.get_formset(request, obj).form + form = self.get_formset(request, obj, fields=None).form fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) return [(None, {'fields': fields})] diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 414d1b4f72..e0f43dfbfe 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -66,12 +66,6 @@ class AdminSite(object): if not admin_class: admin_class = ModelAdmin - # Don't import the humongous validation code unless required - if admin_class and settings.DEBUG: - from django.contrib.admin.validation import validate - else: - validate = lambda model, adminclass: None - if isinstance(model_or_iterable, ModelBase): model_or_iterable = [model_or_iterable] for model in model_or_iterable: @@ -94,8 +88,8 @@ class AdminSite(object): options['__module__'] = __name__ admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - # Validate (which might be a no-op) - validate(admin_class, model) + if admin_class is not ModelAdmin and settings.DEBUG: + admin_class.validate(model) # Instantiate the admin class to save in the registry self._registry[model] = admin_class(model, self) diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index 9d1b917b61..2a1b4d3c90 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -24,7 +24,7 @@ {% if is_popup %}{% endif %} {% if form.errors %}

- {% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}

{% endif %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index daf37753dc..4accf80c46 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -41,7 +41,7 @@ {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} {% if errors %}

- {% blocktrans count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}

{{ adminform.form.non_field_errors }} {% endif %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index c72b6630a3..5d1a6b2714 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -64,7 +64,7 @@ {% endblock %} {% if cl.formset.errors %}

- {% blocktrans count cl.formset.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if cl.formset.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}

{{ cl.formset.non_form_errors }} {% endif %} diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html index 09bc971d2f..24b069cb2f 100644 --- a/django/contrib/admin/templates/admin/includes/fieldset.html +++ b/django/contrib/admin/templates/admin/includes/fieldset.html @@ -7,7 +7,7 @@
{% 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 }} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index 4690363891..1371514d43 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -14,7 +14,7 @@ {% block content %} {% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %}

-{% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} +{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}

{% endif %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index 5cb34739df..f7316a739f 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -17,7 +17,7 @@
{% if form.errors %}

- {% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} + {% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}

{% endif %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index a220f12033..44ae5850b1 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -3,7 +3,7 @@ {% trans "Please go to the following page and choose a new password:" %} {% block reset_link %} -{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb36=uid token=token %} +{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb36=uid token=token %} {% endblock %} {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 18a45a006f..965352e0f5 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -62,7 +62,7 @@ def pagination(cl): # ON_EACH_SIDE links at either end of the "current page" link. page_range = [] if page_num > (ON_EACH_SIDE + ON_ENDS): - page_range.extend(range(0, ON_EACH_SIDE - 1)) + page_range.extend(range(0, ON_ENDS)) page_range.append(DOT) page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1)) else: diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index f70ee2731d..1b9e6aa7ef 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -53,4 +53,4 @@ def get_admin_log(parser, token): if tokens[4] != 'for_user': raise template.TemplateSyntaxError( "Fourth argument to 'get_admin_log' must be 'for_user'") - return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(len(tokens) > 5 and tokens[5] or None)) + return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(tokens[5] if len(tokens) > 5 else None)) diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 97858e688e..a53cd367b4 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -37,9 +37,9 @@ def prepare_lookup_value(key, value): # if key ends with __in, split parameter into separate values if key.endswith('__in'): value = value.split(',') - # if key ends with __isnull, special case '' and false + # if key ends with __isnull, special case '' and the string literals 'false' and '0' if key.endswith('__isnull'): - if value.lower() in ('', 'false'): + if value.lower() in ('', 'false', '0'): value = False else: value = True @@ -269,8 +269,9 @@ def lookup_field(name, obj, model_admin=None): def label_for_field(name, model, model_admin=None, return_attr=False): """ - Returns a sensible label for a field name. The name can be a callable or the - name of an object attributes, as well as a genuine fields. If return_attr is + Returns a sensible label for a field name. The name can be a callable, + property (but not created with @property decorator) or the name of an + object's attribute, as well as a genuine fields. If return_attr is True, the resolved attribute (which could be a callable) is also returned. This will be None if (and only if) the name refers to a field. """ @@ -303,6 +304,10 @@ def label_for_field(name, model, model_admin=None, return_attr=False): if hasattr(attr, "short_description"): label = attr.short_description + elif (isinstance(attr, property) and + hasattr(attr, "fget") and + hasattr(attr.fget, "short_description")): + label = attr.fget.short_description elif callable(attr): if attr.__name__ == "": label = "--" @@ -315,6 +320,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False): else: return label + def help_text_for_field(name, model): try: help_text = model._meta.get_field_by_name(name)[0].help_text diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py index 8d65f96cf1..222d433e53 100644 --- a/django/contrib/admin/validation.py +++ b/django/contrib/admin/validation.py @@ -3,358 +3,405 @@ from django.db import models from django.db.models.fields import FieldDoesNotExist from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model, _get_foreign_key) -from django.contrib.admin import ListFilter, FieldListFilter from django.contrib.admin.util import get_fields_from_path, NotRelationField -from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin, - ModelAdmin, HORIZONTAL, VERTICAL) + +""" +Does basic ModelAdmin option validation. Calls custom validation +classmethod in the end if it is provided in cls. The signature of the +custom validation classmethod should be: def validate(cls, model). +""" + +__all__ = ['BaseValidator', 'InlineValidator'] -__all__ = ['validate'] +class BaseValidator(object): + def __init__(self): + # Before we can introspect models, they need to be fully loaded so that + # inter-relations are set up correctly. We force that here. + models.get_apps() -def validate(cls, model): - """ - Does basic ModelAdmin option validation. Calls custom validation - classmethod in the end if it is provided in cls. The signature of the - custom validation classmethod should be: def validate(cls, model). - """ - # Before we can introspect models, they need to be fully loaded so that - # inter-relations are set up correctly. We force that here. - models.get_apps() + def validate(self, cls, model): + for m in dir(self): + if m.startswith('validate_'): + getattr(self, m)(cls, model) - opts = model._meta - validate_base(cls, model) + def check_field_spec(self, cls, model, flds, label): + """ + Validate the fields specification in `flds` from a ModelAdmin subclass + `cls` for the `model` model. Use `label` for reporting problems to the user. - # list_display - if hasattr(cls, 'list_display'): - check_isseq(cls, 'list_display', cls.list_display) - for idx, field in enumerate(cls.list_display): - if not callable(field): - if not hasattr(cls, field): - if not hasattr(model, field): - try: - opts.get_field(field) - except models.FieldDoesNotExist: - raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." - % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) - else: - # getattr(model, field) could be an X_RelatedObjectsDescriptor - f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) - if isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." - % (cls.__name__, idx, field)) - - # list_display_links - if hasattr(cls, 'list_display_links'): - check_isseq(cls, 'list_display_links', cls.list_display_links) - for idx, field in enumerate(cls.list_display_links): - if field not in cls.list_display: - raise ImproperlyConfigured("'%s.list_display_links[%d]' " - "refers to '%s' which is not defined in 'list_display'." - % (cls.__name__, idx, field)) - - # list_filter - if hasattr(cls, 'list_filter'): - check_isseq(cls, 'list_filter', cls.list_filter) - for idx, item in enumerate(cls.list_filter): - # There are three options for specifying a filter: - # 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel') - # 2: ('field', SomeFieldListFilter) - a field-based list filter class - # 3: SomeListFilter - a non-field list filter class - if callable(item) and not isinstance(item, models.Field): - # If item is option 3, it should be a ListFilter... - if not issubclass(item, ListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" - " which is not a descendant of ListFilter." - % (cls.__name__, idx, item.__name__)) - # ... but not a FieldListFilter. - if issubclass(item, FieldListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" - " which is of type FieldListFilter but is not" - " associated with a field name." - % (cls.__name__, idx, item.__name__)) - else: - if isinstance(item, (tuple, list)): - # item is option #2 - field, list_filter_class = item - if not issubclass(list_filter_class, FieldListFilter): - raise ImproperlyConfigured("'%s.list_filter[%d][1]'" - " is '%s' which is not of type FieldListFilter." - % (cls.__name__, idx, list_filter_class.__name__)) - else: - # item is option #1 - field = item - # Validate the field string + The fields specification can be a ``fields`` option or a ``fields`` + sub-option from a ``fieldsets`` option component. + """ + for fields in flds: + # The entry in fields might be a tuple. If it is a standalone + # field, make it into a tuple to make processing easier. + if type(fields) != tuple: + fields = (fields,) + for field in fields: + if field in cls.readonly_fields: + # Stuff can be put in fields that isn't actually a + # model field if it's in readonly_fields, + # readonly_fields will handle the validation of such + # things. + continue try: - get_fields_from_path(model, field) - except (NotRelationField, FieldDoesNotExist): - raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" - " which does not refer to a Field." + f = model._meta.get_field(field) + except models.FieldDoesNotExist: + # If we can't find a field on the model that matches, it could be an + # extra field on the form; nothing to check so move on to the next field. + continue + if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: + raise ImproperlyConfigured("'%s.%s' " + "can't include the ManyToManyField field '%s' because " + "'%s' manually specifies a 'through' model." % ( + cls.__name__, label, field, field)) + + def validate_raw_id_fields(self, cls, model): + " Validate that raw_id_fields only contains field names that are listed on the model. " + if hasattr(cls, 'raw_id_fields'): + check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) + for idx, field in enumerate(cls.raw_id_fields): + f = get_field(cls, model, 'raw_id_fields', field) + if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): + raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " + "be either a ForeignKey or ManyToManyField." % (cls.__name__, idx, field)) - # list_per_page = 100 - if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int): - raise ImproperlyConfigured("'%s.list_per_page' should be a integer." - % cls.__name__) + def validate_fields(self, cls, model): + " Validate that fields only refer to existing fields, doesn't contain duplicates. " + # fields + if cls.fields: # default value is None + check_isseq(cls, 'fields', cls.fields) + self.check_field_spec(cls, model, cls.fields, 'fields') + if cls.fieldsets: + raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) + if len(cls.fields) > len(set(cls.fields)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) - # list_max_show_all - if hasattr(cls, 'list_max_show_all') and not isinstance(cls.list_max_show_all, int): - raise ImproperlyConfigured("'%s.list_max_show_all' should be an integer." - % cls.__name__) + def validate_fieldsets(self, cls, model): + " Validate that fieldsets is properly formatted and doesn't contain duplicates. " + from django.contrib.admin.options import flatten_fieldsets + if cls.fieldsets: # default value is None + check_isseq(cls, 'fieldsets', cls.fieldsets) + for idx, fieldset in enumerate(cls.fieldsets): + check_isseq(cls, 'fieldsets[%d]' % idx, fieldset) + if len(fieldset) != 2: + raise ImproperlyConfigured("'%s.fieldsets[%d]' does not " + "have exactly two elements." % (cls.__name__, idx)) + check_isdict(cls, 'fieldsets[%d][1]' % idx, fieldset[1]) + if 'fields' not in fieldset[1]: + raise ImproperlyConfigured("'fields' key is required in " + "%s.fieldsets[%d][1] field options dict." + % (cls.__name__, idx)) + self.check_field_spec(cls, model, fieldset[1]['fields'], "fieldsets[%d][1]['fields']" % idx) + flattened_fieldsets = flatten_fieldsets(cls.fieldsets) + if len(flattened_fieldsets) > len(set(flattened_fieldsets)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) - # list_editable - if hasattr(cls, 'list_editable') and cls.list_editable: - check_isseq(cls, 'list_editable', cls.list_editable) - for idx, field_name in enumerate(cls.list_editable): - try: - field = opts.get_field_by_name(field_name)[0] - except models.FieldDoesNotExist: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " - "field, '%s', not defined on %s.%s." - % (cls.__name__, idx, field_name, model._meta.app_label, model.__name__)) - if field_name not in cls.list_display: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " - "'%s' which is not defined in 'list_display'." - % (cls.__name__, idx, field_name)) - if field_name in cls.list_display_links: - raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" - " and '%s.list_display_links'" - % (field_name, cls.__name__, cls.__name__)) - if not cls.list_display_links and cls.list_display[0] in cls.list_editable: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" - " the first field in list_display, '%s', which can't be" - " used unless list_display_links is set." - % (cls.__name__, idx, cls.list_display[0])) - if not field.editable: - raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " - "field, '%s', which isn't editable through the admin." - % (cls.__name__, idx, field_name)) + def validate_exclude(self, cls, model): + " Validate that exclude is a sequence without duplicates. " + if cls.exclude: # default value is None + check_isseq(cls, 'exclude', cls.exclude) + if len(cls.exclude) > len(set(cls.exclude)): + raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__) - # search_fields = () - if hasattr(cls, 'search_fields'): - check_isseq(cls, 'search_fields', cls.search_fields) + def validate_form(self, cls, model): + " Validate that form subclasses BaseModelForm. " + if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): + raise ImproperlyConfigured("%s.form does not inherit from " + "BaseModelForm." % cls.__name__) - # date_hierarchy = None - if cls.date_hierarchy: - f = get_field(cls, model, opts, 'date_hierarchy', cls.date_hierarchy) - if not isinstance(f, (models.DateField, models.DateTimeField)): - raise ImproperlyConfigured("'%s.date_hierarchy is " - "neither an instance of DateField nor DateTimeField." - % cls.__name__) + def validate_filter_vertical(self, cls, model): + " Validate that filter_vertical is a sequence of field names. " + if hasattr(cls, 'filter_vertical'): + check_isseq(cls, 'filter_vertical', cls.filter_vertical) + for idx, field in enumerate(cls.filter_vertical): + f = get_field(cls, model, 'filter_vertical', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " + "a ManyToManyField." % (cls.__name__, idx)) - # ordering = None - if cls.ordering: - check_isseq(cls, 'ordering', cls.ordering) - for idx, field in enumerate(cls.ordering): - if field == '?' and len(cls.ordering) != 1: - raise ImproperlyConfigured("'%s.ordering' has the random " - "ordering marker '?', but contains other fields as " - "well. Please either remove '?' or the other fields." + def validate_filter_horizontal(self, cls, model): + " Validate that filter_horizontal is a sequence of field names. " + if hasattr(cls, 'filter_horizontal'): + check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) + for idx, field in enumerate(cls.filter_horizontal): + f = get_field(cls, model, 'filter_horizontal', field) + if not isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " + "a ManyToManyField." % (cls.__name__, idx)) + + def validate_radio_fields(self, cls, model): + " Validate that radio_fields is a dictionary of choice or foreign key fields. " + from django.contrib.admin.options import HORIZONTAL, VERTICAL + if hasattr(cls, 'radio_fields'): + check_isdict(cls, 'radio_fields', cls.radio_fields) + for field, val in cls.radio_fields.items(): + f = get_field(cls, model, 'radio_fields', field) + if not (isinstance(f, models.ForeignKey) or f.choices): + raise ImproperlyConfigured("'%s.radio_fields['%s']' " + "is neither an instance of ForeignKey nor does " + "have choices set." % (cls.__name__, field)) + if not val in (HORIZONTAL, VERTICAL): + raise ImproperlyConfigured("'%s.radio_fields['%s']' " + "is neither admin.HORIZONTAL nor admin.VERTICAL." + % (cls.__name__, field)) + + def validate_prepopulated_fields(self, cls, model): + " Validate that prepopulated_fields if a dictionary containing allowed field types. " + # prepopulated_fields + if hasattr(cls, 'prepopulated_fields'): + check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) + for field, val in cls.prepopulated_fields.items(): + f = get_field(cls, model, 'prepopulated_fields', field) + if isinstance(f, (models.DateTimeField, models.ForeignKey, + models.ManyToManyField)): + raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " + "is either a DateTimeField, ForeignKey or " + "ManyToManyField. This isn't allowed." + % (cls.__name__, field)) + check_isseq(cls, "prepopulated_fields['%s']" % field, val) + for idx, f in enumerate(val): + get_field(cls, model, "prepopulated_fields['%s'][%d]" % (field, idx), f) + + def validate_ordering(self, cls, model): + " Validate that ordering refers to existing fields or is random. " + # ordering = None + if cls.ordering: + check_isseq(cls, 'ordering', cls.ordering) + for idx, field in enumerate(cls.ordering): + if field == '?' and len(cls.ordering) != 1: + raise ImproperlyConfigured("'%s.ordering' has the random " + "ordering marker '?', but contains other fields as " + "well. Please either remove '?' or the other fields." + % cls.__name__) + if field == '?': + continue + if field.startswith('-'): + field = field[1:] + # Skip ordering in the format field1__field2 (FIXME: checking + # this format would be nice, but it's a little fiddly). + if '__' in field: + continue + get_field(cls, model, 'ordering[%d]' % idx, field) + + def validate_readonly_fields(self, cls, model): + " Validate that readonly_fields refers to proper attribute or field. " + if hasattr(cls, "readonly_fields"): + check_isseq(cls, "readonly_fields", cls.readonly_fields) + for idx, field in enumerate(cls.readonly_fields): + if not callable(field): + if not hasattr(cls, field): + if not hasattr(model, field): + try: + model._meta.get_field(field) + except models.FieldDoesNotExist: + raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r." + % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) + + +class ModelAdminValidator(BaseValidator): + def validate_save_as(self, cls, model): + " Validate save_as is a boolean. " + check_type(cls, 'save_as', bool) + + def validate_save_on_top(self, cls, model): + " Validate save_on_top is a boolean. " + check_type(cls, 'save_on_top', bool) + + def validate_inlines(self, cls, model): + " Validate inline model admin classes. " + from django.contrib.admin.options import BaseModelAdmin + if hasattr(cls, 'inlines'): + check_isseq(cls, 'inlines', cls.inlines) + for idx, inline in enumerate(cls.inlines): + if not issubclass(inline, BaseModelAdmin): + raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " + "from BaseModelAdmin." % (cls.__name__, idx)) + if not inline.model: + raise ImproperlyConfigured("'model' is a required attribute " + "of '%s.inlines[%d]'." % (cls.__name__, idx)) + if not issubclass(inline.model, models.Model): + raise ImproperlyConfigured("'%s.inlines[%d].model' does not " + "inherit from models.Model." % (cls.__name__, idx)) + inline.validate(inline.model) + self.check_inline(inline, model) + + def check_inline(self, cls, parent_model): + " Validate inline class's fk field is not excluded. " + fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + if hasattr(cls, 'exclude') and cls.exclude: + if fk and fk.name in cls.exclude: + raise ImproperlyConfigured("%s cannot exclude the field " + "'%s' - this is the foreign key to the parent model " + "%s.%s." % (cls.__name__, fk.name, parent_model._meta.app_label, parent_model.__name__)) + + def validate_list_display(self, cls, model): + " Validate that list_display only contains fields or usable attributes. " + if hasattr(cls, 'list_display'): + check_isseq(cls, 'list_display', cls.list_display) + for idx, field in enumerate(cls.list_display): + if not callable(field): + if not hasattr(cls, field): + if not hasattr(model, field): + try: + model._meta.get_field(field) + except models.FieldDoesNotExist: + raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r." + % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) + else: + # getattr(model, field) could be an X_RelatedObjectsDescriptor + f = fetch_attr(cls, model, "list_display[%d]" % idx, field) + if isinstance(f, models.ManyToManyField): + raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." + % (cls.__name__, idx, field)) + + def validate_list_display_links(self, cls, model): + " Validate that list_display_links is a unique subset of list_display. " + if hasattr(cls, 'list_display_links'): + check_isseq(cls, 'list_display_links', cls.list_display_links) + for idx, field in enumerate(cls.list_display_links): + if field not in cls.list_display: + raise ImproperlyConfigured("'%s.list_display_links[%d]' " + "refers to '%s' which is not defined in 'list_display'." + % (cls.__name__, idx, field)) + + def validate_list_filter(self, cls, model): + """ + Validate that list_filter is a sequence of one of three options: + 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel') + 2: ('field', SomeFieldListFilter) - a field-based list filter class + 3: SomeListFilter - a non-field list filter class + """ + from django.contrib.admin import ListFilter, FieldListFilter + if hasattr(cls, 'list_filter'): + check_isseq(cls, 'list_filter', cls.list_filter) + for idx, item in enumerate(cls.list_filter): + if callable(item) and not isinstance(item, models.Field): + # If item is option 3, it should be a ListFilter... + if not issubclass(item, ListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" + " which is not a descendant of ListFilter." + % (cls.__name__, idx, item.__name__)) + # ... but not a FieldListFilter. + if issubclass(item, FieldListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'" + " which is of type FieldListFilter but is not" + " associated with a field name." + % (cls.__name__, idx, item.__name__)) + else: + if isinstance(item, (tuple, list)): + # item is option #2 + field, list_filter_class = item + if not issubclass(list_filter_class, FieldListFilter): + raise ImproperlyConfigured("'%s.list_filter[%d][1]'" + " is '%s' which is not of type FieldListFilter." + % (cls.__name__, idx, list_filter_class.__name__)) + else: + # item is option #1 + field = item + # Validate the field string + try: + get_fields_from_path(model, field) + except (NotRelationField, FieldDoesNotExist): + raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'" + " which does not refer to a Field." + % (cls.__name__, idx, field)) + + def validate_list_select_related(self, cls, model): + " Validate that list_select_related is a boolean, a list or a tuple. " + list_select_related = getattr(cls, 'list_select_related', None) + if list_select_related: + types = (bool, tuple, list) + if not isinstance(list_select_related, types): + raise ImproperlyConfigured("'%s.list_select_related' should be " + "either a bool, a tuple or a list" % + cls.__name__) + + def validate_list_per_page(self, cls, model): + " Validate that list_per_page is an integer. " + check_type(cls, 'list_per_page', int) + + def validate_list_max_show_all(self, cls, model): + " Validate that list_max_show_all is an integer. " + check_type(cls, 'list_max_show_all', int) + + def validate_list_editable(self, cls, model): + """ + Validate that list_editable is a sequence of editable fields from + list_display without first element. + """ + if hasattr(cls, 'list_editable') and cls.list_editable: + check_isseq(cls, 'list_editable', cls.list_editable) + for idx, field_name in enumerate(cls.list_editable): + try: + field = model._meta.get_field_by_name(field_name)[0] + except models.FieldDoesNotExist: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " + "field, '%s', not defined on %s.%s." + % (cls.__name__, idx, field_name, model._meta.app_label, model.__name__)) + if field_name not in cls.list_display: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to " + "'%s' which is not defined in 'list_display'." + % (cls.__name__, idx, field_name)) + if field_name in cls.list_display_links: + raise ImproperlyConfigured("'%s' cannot be in both '%s.list_editable'" + " and '%s.list_display_links'" + % (field_name, cls.__name__, cls.__name__)) + if not cls.list_display_links and cls.list_display[0] in cls.list_editable: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to" + " the first field in list_display, '%s', which can't be" + " used unless list_display_links is set." + % (cls.__name__, idx, cls.list_display[0])) + if not field.editable: + raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a " + "field, '%s', which isn't editable through the admin." + % (cls.__name__, idx, field_name)) + + def validate_search_fields(self, cls, model): + " Validate search_fields is a sequence. " + if hasattr(cls, 'search_fields'): + check_isseq(cls, 'search_fields', cls.search_fields) + + def validate_date_hierarchy(self, cls, model): + " Validate that date_hierarchy refers to DateField or DateTimeField. " + if cls.date_hierarchy: + f = get_field(cls, model, 'date_hierarchy', cls.date_hierarchy) + if not isinstance(f, (models.DateField, models.DateTimeField)): + raise ImproperlyConfigured("'%s.date_hierarchy is " + "neither an instance of DateField nor DateTimeField." % cls.__name__) - if field == '?': - continue - if field.startswith('-'): - field = field[1:] - # Skip ordering in the format field1__field2 (FIXME: checking - # this format would be nice, but it's a little fiddly). - if '__' in field: - continue - get_field(cls, model, opts, 'ordering[%d]' % idx, field) - - if hasattr(cls, "readonly_fields"): - check_readonly_fields(cls, model, opts) - - # list_select_related = False - # save_as = False - # save_on_top = False - for attr in ('list_select_related', 'save_as', 'save_on_top'): - if not isinstance(getattr(cls, attr), bool): - raise ImproperlyConfigured("'%s.%s' should be a boolean." - % (cls.__name__, attr)) - # inlines = [] - if hasattr(cls, 'inlines'): - check_isseq(cls, 'inlines', cls.inlines) - for idx, inline in enumerate(cls.inlines): - if not issubclass(inline, BaseModelAdmin): - raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit " - "from BaseModelAdmin." % (cls.__name__, idx)) - if not inline.model: - raise ImproperlyConfigured("'model' is a required attribute " - "of '%s.inlines[%d]'." % (cls.__name__, idx)) - if not issubclass(inline.model, models.Model): - raise ImproperlyConfigured("'%s.inlines[%d].model' does not " - "inherit from models.Model." % (cls.__name__, idx)) - validate_base(inline, inline.model) - validate_inline(inline, cls, model) +class InlineValidator(BaseValidator): + def validate_fk_name(self, cls, model): + " Validate that fk_name refers to a ForeignKey. " + if cls.fk_name: # default value is None + f = get_field(cls, model, 'fk_name', cls.fk_name) + if not isinstance(f, models.ForeignKey): + raise ImproperlyConfigured("'%s.fk_name is not an instance of " + "models.ForeignKey." % cls.__name__) -def validate_inline(cls, parent, parent_model): + def validate_extra(self, cls, model): + " Validate that extra is an integer. " + check_type(cls, 'extra', int) - # model is already verified to exist and be a Model - if cls.fk_name: # default value is None - f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name) - if not isinstance(f, models.ForeignKey): - raise ImproperlyConfigured("'%s.fk_name is not an instance of " - "models.ForeignKey." % cls.__name__) + def validate_max_num(self, cls, model): + " Validate that max_num is an integer. " + check_type(cls, 'max_num', int) - fk = _get_foreign_key(parent_model, cls.model, fk_name=cls.fk_name, can_fail=True) + def validate_formset(self, cls, model): + " Validate formset is a subclass of BaseModelFormSet. " + if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): + raise ImproperlyConfigured("'%s.formset' does not inherit from " + "BaseModelFormSet." % cls.__name__) - # extra = 3 - if not isinstance(cls.extra, int): - raise ImproperlyConfigured("'%s.extra' should be a integer." - % cls.__name__) - # max_num = None - max_num = getattr(cls, 'max_num', None) - if max_num is not None and not isinstance(max_num, int): - raise ImproperlyConfigured("'%s.max_num' should be an integer or None (default)." - % cls.__name__) - - # formset - if hasattr(cls, 'formset') and not issubclass(cls.formset, BaseModelFormSet): - raise ImproperlyConfigured("'%s.formset' does not inherit from " - "BaseModelFormSet." % cls.__name__) - - # exclude - if hasattr(cls, 'exclude') and cls.exclude: - if fk and fk.name in cls.exclude: - raise ImproperlyConfigured("%s cannot exclude the field " - "'%s' - this is the foreign key to the parent model " - "%s.%s." % (cls.__name__, fk.name, parent_model._meta.app_label, parent_model.__name__)) - - if hasattr(cls, "readonly_fields"): - check_readonly_fields(cls, cls.model, cls.model._meta) - -def validate_fields_spec(cls, model, opts, flds, label): - """ - Validate the fields specification in `flds` from a ModelAdmin subclass - `cls` for the `model` model. `opts` is `model`'s Meta inner class. - Use `label` for reporting problems to the user. - - The fields specification can be a ``fields`` option or a ``fields`` - sub-option from a ``fieldsets`` option component. - """ - for fields in flds: - # The entry in fields might be a tuple. If it is a standalone - # field, make it into a tuple to make processing easier. - if type(fields) != tuple: - fields = (fields,) - for field in fields: - if field in cls.readonly_fields: - # Stuff can be put in fields that isn't actually a - # model field if it's in readonly_fields, - # readonly_fields will handle the validation of such - # things. - continue - try: - f = opts.get_field(field) - except models.FieldDoesNotExist: - # If we can't find a field on the model that matches, it could be an - # extra field on the form; nothing to check so move on to the next field. - continue - if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created: - raise ImproperlyConfigured("'%s.%s' " - "can't include the ManyToManyField field '%s' because " - "'%s' manually specifies a 'through' model." % ( - cls.__name__, label, field, field)) - -def validate_base(cls, model): - opts = model._meta - - # raw_id_fields - if hasattr(cls, 'raw_id_fields'): - check_isseq(cls, 'raw_id_fields', cls.raw_id_fields) - for idx, field in enumerate(cls.raw_id_fields): - f = get_field(cls, model, opts, 'raw_id_fields', field) - if not isinstance(f, (models.ForeignKey, models.ManyToManyField)): - raise ImproperlyConfigured("'%s.raw_id_fields[%d]', '%s' must " - "be either a ForeignKey or ManyToManyField." - % (cls.__name__, idx, field)) - - # fields - if cls.fields: # default value is None - check_isseq(cls, 'fields', cls.fields) - validate_fields_spec(cls, model, opts, cls.fields, 'fields') - if cls.fieldsets: - raise ImproperlyConfigured('Both fieldsets and fields are specified in %s.' % cls.__name__) - if len(cls.fields) > len(set(cls.fields)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.fields' % cls.__name__) - - # fieldsets - if cls.fieldsets: # default value is None - check_isseq(cls, 'fieldsets', cls.fieldsets) - for idx, fieldset in enumerate(cls.fieldsets): - check_isseq(cls, 'fieldsets[%d]' % idx, fieldset) - if len(fieldset) != 2: - raise ImproperlyConfigured("'%s.fieldsets[%d]' does not " - "have exactly two elements." % (cls.__name__, idx)) - check_isdict(cls, 'fieldsets[%d][1]' % idx, fieldset[1]) - if 'fields' not in fieldset[1]: - raise ImproperlyConfigured("'fields' key is required in " - "%s.fieldsets[%d][1] field options dict." - % (cls.__name__, idx)) - validate_fields_spec(cls, model, opts, fieldset[1]['fields'], "fieldsets[%d][1]['fields']" % idx) - flattened_fieldsets = flatten_fieldsets(cls.fieldsets) - if len(flattened_fieldsets) > len(set(flattened_fieldsets)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.fieldsets' % cls.__name__) - - # exclude - if cls.exclude: # default value is None - check_isseq(cls, 'exclude', cls.exclude) - if len(cls.exclude) > len(set(cls.exclude)): - raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__) - - # form - if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm): - raise ImproperlyConfigured("%s.form does not inherit from " - "BaseModelForm." % cls.__name__) - - # filter_vertical - if hasattr(cls, 'filter_vertical'): - check_isseq(cls, 'filter_vertical', cls.filter_vertical) - for idx, field in enumerate(cls.filter_vertical): - f = get_field(cls, model, opts, 'filter_vertical', field) - if not isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " - "a ManyToManyField." % (cls.__name__, idx)) - - # filter_horizontal - if hasattr(cls, 'filter_horizontal'): - check_isseq(cls, 'filter_horizontal', cls.filter_horizontal) - for idx, field in enumerate(cls.filter_horizontal): - f = get_field(cls, model, opts, 'filter_horizontal', field) - if not isinstance(f, models.ManyToManyField): - raise ImproperlyConfigured("'%s.filter_horizontal[%d]' must be " - "a ManyToManyField." % (cls.__name__, idx)) - - # radio_fields - if hasattr(cls, 'radio_fields'): - check_isdict(cls, 'radio_fields', cls.radio_fields) - for field, val in cls.radio_fields.items(): - f = get_field(cls, model, opts, 'radio_fields', field) - if not (isinstance(f, models.ForeignKey) or f.choices): - raise ImproperlyConfigured("'%s.radio_fields['%s']' " - "is neither an instance of ForeignKey nor does " - "have choices set." % (cls.__name__, field)) - if not val in (HORIZONTAL, VERTICAL): - raise ImproperlyConfigured("'%s.radio_fields['%s']' " - "is neither admin.HORIZONTAL nor admin.VERTICAL." - % (cls.__name__, field)) - - # prepopulated_fields - if hasattr(cls, 'prepopulated_fields'): - check_isdict(cls, 'prepopulated_fields', cls.prepopulated_fields) - for field, val in cls.prepopulated_fields.items(): - f = get_field(cls, model, opts, 'prepopulated_fields', field) - if isinstance(f, (models.DateTimeField, models.ForeignKey, - models.ManyToManyField)): - raise ImproperlyConfigured("'%s.prepopulated_fields['%s']' " - "is either a DateTimeField, ForeignKey or " - "ManyToManyField. This isn't allowed." - % (cls.__name__, field)) - check_isseq(cls, "prepopulated_fields['%s']" % field, val) - for idx, f in enumerate(val): - get_field(cls, model, opts, "prepopulated_fields['%s'][%d]" % (field, idx), f) +def check_type(cls, attr, type_): + if getattr(cls, attr, None) is not None and not isinstance(getattr(cls, attr), type_): + raise ImproperlyConfigured("'%s.%s' should be a %s." + % (cls.__name__, attr, type_.__name__ )) def check_isseq(cls, label, obj): if not isinstance(obj, (list, tuple)): @@ -364,16 +411,16 @@ def check_isdict(cls, label, obj): if not isinstance(obj, dict): raise ImproperlyConfigured("'%s.%s' must be a dictionary." % (cls.__name__, label)) -def get_field(cls, model, opts, label, field): +def get_field(cls, model, label, field): try: - return opts.get_field(field) + return model._meta.get_field(field) except models.FieldDoesNotExist: raise ImproperlyConfigured("'%s.%s' refers to field '%s' that is missing from model '%s.%s'." % (cls.__name__, label, field, model._meta.app_label, model.__name__)) -def fetch_attr(cls, model, opts, label, field): +def fetch_attr(cls, model, label, field): try: - return opts.get_field(field) + return model._meta.get_field(field) except models.FieldDoesNotExist: pass try: @@ -381,15 +428,3 @@ def fetch_attr(cls, model, opts, label, field): except AttributeError: raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s.%s'." % (cls.__name__, label, field, model._meta.app_label, model.__name__)) - -def check_readonly_fields(cls, model, opts): - check_isseq(cls, "readonly_fields", cls.readonly_fields) - for idx, field in enumerate(cls.readonly_fields): - if not callable(field): - if not hasattr(cls, field): - if not hasattr(model, field): - try: - opts.get_field(field) - except models.FieldDoesNotExist: - raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r." - % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 050d4776d0..8ea7e10fc0 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,7 +1,5 @@ -import operator import sys import warnings -from functools import reduce from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured from django.core.paginator import InvalidPage @@ -16,6 +14,7 @@ from django.utils.translation import ugettext, ugettext_lazy from django.utils.http import urlencode from django.contrib.admin import FieldListFilter +from django.contrib.admin.exceptions import DisallowedModelAdminLookup from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.util import (quote, get_fields_from_path, lookup_needs_distinct, prepare_lookup_value) @@ -130,7 +129,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): lookup_params[force_str(key)] = value if not self.model_admin.lookup_allowed(key, value): - raise SuspiciousOperation("Filtering by %s not allowed" % key) + raise DisallowedModelAdminLookup("Filtering by %s not allowed" % key) filter_specs = [] if self.list_filter: @@ -331,7 +330,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): def get_queryset(self, request): # First, we collect all the declared list filters. (self.filter_specs, self.has_filters, remaining_lookup_params, - use_distinct) = self.get_filters(request) + filters_use_distinct) = self.get_filters(request) # Then, we let every list filter modify the queryset to its liking. qs = self.root_queryset @@ -357,56 +356,46 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)): # ValueError, ValidationError, or ?. raise IncorrectLookupParameters(e) - # Use select_related() if one of the list_display options is a field - # with a relationship and the provided queryset doesn't already have - # select_related defined. if not qs.query.select_related: - if self.list_select_related: - qs = qs.select_related() - else: - for field_name in self.list_display: - try: - field = self.lookup_opts.get_field(field_name) - except models.FieldDoesNotExist: - pass - else: - if isinstance(field.rel, models.ManyToOneRel): - qs = qs.select_related() - break + qs = self.apply_select_related(qs) # Set ordering. ordering = self.get_ordering(request, qs) qs = qs.order_by(*ordering) - # Apply keyword searches. - def construct_search(field_name): - if field_name.startswith('^'): - return "%s__istartswith" % field_name[1:] - elif field_name.startswith('='): - return "%s__iexact" % field_name[1:] - elif field_name.startswith('@'): - return "%s__search" % field_name[1:] - else: - return "%s__icontains" % field_name + # Apply search results + qs, search_use_distinct = self.model_admin.get_search_results( + request, qs, self.query) - if self.search_fields and self.query: - orm_lookups = [construct_search(str(search_field)) - for search_field in self.search_fields] - for bit in self.query.split(): - or_queries = [models.Q(**{orm_lookup: bit}) - for orm_lookup in orm_lookups] - qs = qs.filter(reduce(operator.or_, or_queries)) - if not use_distinct: - for search_spec in orm_lookups: - if lookup_needs_distinct(self.lookup_opts, search_spec): - use_distinct = True - break - - if use_distinct: + # Remove duplicates from results, if necessary + if filters_use_distinct | search_use_distinct: return qs.distinct() else: return qs + def apply_select_related(self, qs): + if self.list_select_related is True: + return qs.select_related() + + if self.list_select_related is False: + if self.has_related_field_in_list_display(): + return qs.select_related() + + if self.list_select_related: + return qs.select_related(*self.list_select_related) + return qs + + def has_related_field_in_list_display(self): + for field_name in self.list_display: + try: + field = self.lookup_opts.get_field(field_name) + except models.FieldDoesNotExist: + pass + else: + if isinstance(field.rel, models.ManyToOneRel): + return True + return False + def url_for_result(self, result): pk = getattr(result, self.pk_attname) return reverse('admin:%s_%s_change' % (self.opts.app_label, diff --git a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po index 2e4dcdf13d..5a4dcd0872 100644 --- a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po +++ b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: Django\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-05-02 16:18+0200\n" +"POT-Creation-Date: 2013-06-02 00:30-0400\n" "PO-Revision-Date: 2010-05-13 15:35+0200\n" "Last-Translator: Django team\n" "Language-Team: English \n" @@ -60,12 +60,13 @@ msgstr "" msgid "number of %s" msgstr "" -#: views.py:270 +#. Translators: %s is an object type name +#: views.py:271 #, python-format -msgid "Fields on %s objects" +msgid "Attributes on %s objects" msgstr "" -#: views.py:362 +#: views.py:363 #, python-format msgid "%s does not appear to be a urlpattern object" msgstr "" @@ -83,7 +84,9 @@ msgid "Home" msgstr "" #: templates/admin_doc/bookmarklets.html:7 templates/admin_doc/index.html:7 +#: templates/admin_doc/index.html.py:10 templates/admin_doc/index.html:14 #: templates/admin_doc/missing_docutils.html:7 +#: templates/admin_doc/missing_docutils.html:14 #: templates/admin_doc/model_detail.html:15 #: templates/admin_doc/model_index.html:9 #: templates/admin_doc/template_detail.html:7 @@ -94,7 +97,7 @@ msgstr "" msgid "Documentation" msgstr "" -#: templates/admin_doc/bookmarklets.html:8 +#: templates/admin_doc/bookmarklets.html:8 templates/admin_doc/index.html:29 msgid "Bookmarklets" msgstr "" @@ -149,26 +152,202 @@ msgstr "" msgid "As above, but opens the admin page in a new window." msgstr "" -#: templates/admin_doc/model_detail.html:16 +#: templates/admin_doc/index.html:17 +#: templates/admin_doc/template_tag_index.html:9 +msgid "Tags" +msgstr "" + +#: templates/admin_doc/index.html:18 +msgid "List of all the template tags and their functions." +msgstr "" + +#: templates/admin_doc/index.html:20 +#: templates/admin_doc/template_filter_index.html:9 +msgid "Filters" +msgstr "" + +#: templates/admin_doc/index.html:21 +msgid "" +"Filters are actions which can be applied to variables in a template to alter " +"the output." +msgstr "" + +#: templates/admin_doc/index.html:23 templates/admin_doc/model_detail.html:16 #: templates/admin_doc/model_index.html:10 +#: templates/admin_doc/model_index.html:14 msgid "Models" msgstr "" +#: templates/admin_doc/index.html:24 +msgid "" +"Models are descriptions of all the objects in the system and their " +"associated fields. Each model has a list of fields which can be accessed as " +"template variables" +msgstr "" + +#: templates/admin_doc/index.html:26 templates/admin_doc/view_detail.html:8 +#: templates/admin_doc/view_index.html:9 +#: templates/admin_doc/view_index.html:12 +msgid "Views" +msgstr "" + +#: templates/admin_doc/index.html:27 +msgid "" +"Each page on the public site is generated by a view. The view defines which " +"template is used to generate the page and which objects are available to " +"that template." +msgstr "" + +#: templates/admin_doc/index.html:30 +msgid "Tools for your browser to quickly access admin functionality." +msgstr "" + +#: templates/admin_doc/missing_docutils.html:10 +msgid "Please install docutils" +msgstr "" + +#: templates/admin_doc/missing_docutils.html:17 +#, python-format +msgid "" +"The admin documentation system requires Python's docutils library." +msgstr "" + +#: templates/admin_doc/missing_docutils.html:19 +#, python-format +msgid "" +"Please ask your administrators to install docutils." +msgstr "" + +#: templates/admin_doc/model_detail.html:21 +#, python-format +msgid "Model: %(name)s" +msgstr "" + +#: templates/admin_doc/model_detail.html:35 +msgid "Field" +msgstr "" + +#: templates/admin_doc/model_detail.html:36 +msgid "Type" +msgstr "" + +#: templates/admin_doc/model_detail.html:37 +msgid "Description" +msgstr "" + +#: templates/admin_doc/model_detail.html:52 +msgid "Back to Models Documentation" +msgstr "" + +#: templates/admin_doc/model_index.html:18 +msgid "Model documentation" +msgstr "" + +#: templates/admin_doc/model_index.html:43 +msgid "Model groups" +msgstr "" + #: templates/admin_doc/template_detail.html:8 msgid "Templates" msgstr "" -#: templates/admin_doc/template_filter_index.html:9 -msgid "Filters" +#: templates/admin_doc/template_detail.html:13 +#, python-format +msgid "Template: %(name)s" msgstr "" -#: templates/admin_doc/template_tag_index.html:9 -msgid "Tags" +#: templates/admin_doc/template_detail.html:16 +#, python-format +msgid "Template: \"%(name)s\"" msgstr "" -#: templates/admin_doc/view_detail.html:8 -#: templates/admin_doc/view_index.html:9 -msgid "Views" +#: templates/admin_doc/template_detail.html:20 +#, python-format +msgid "Search path for template \"%(name)s\" on %(grouper)s:" +msgstr "" + +#: templates/admin_doc/template_detail.html:23 +msgid "(does not exist)" +msgstr "" + +#: templates/admin_doc/template_detail.html:28 +msgid "Back to Documentation" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:12 +msgid "Template filters" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:16 +msgid "Template filter documentation" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:22 +#: templates/admin_doc/template_filter_index.html:43 +msgid "Built-in filters" +msgstr "" + +#: templates/admin_doc/template_filter_index.html:23 +#, python-format +msgid "" +"To use these filters, put %(code)s in your template before " +"using the filter." +msgstr "" + +#: templates/admin_doc/template_tag_index.html:12 +msgid "Template tags" +msgstr "" + +#: templates/admin_doc/template_tag_index.html:16 +msgid "Template tag documentation" +msgstr "" + +#: templates/admin_doc/template_tag_index.html:22 +#: templates/admin_doc/template_tag_index.html:43 +msgid "Built-in tags" +msgstr "" + +#: templates/admin_doc/template_tag_index.html:23 +#, python-format +msgid "" +"To use these tags, put %(code)s in your template before using " +"the tag." +msgstr "" + +#: templates/admin_doc/view_detail.html:12 +#, python-format +msgid "View: %(name)s" +msgstr "" + +#: templates/admin_doc/view_detail.html:23 +msgid "Context:" +msgstr "" + +#: templates/admin_doc/view_detail.html:28 +msgid "Templates:" +msgstr "" + +#: templates/admin_doc/view_detail.html:32 +msgid "Back to Views Documentation" +msgstr "" + +#: templates/admin_doc/view_index.html:16 +msgid "View documentation" +msgstr "" + +#: templates/admin_doc/view_index.html:22 +msgid "Jump to site" +msgstr "" + +#: templates/admin_doc/view_index.html:35 +#, python-format +msgid "Views by URL on %(name)s" +msgstr "" + +#: templates/admin_doc/view_index.html:40 +#, python-format +msgid "View function: %(name)s" msgstr "" #: tests/test_fields.py:29 diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py new file mode 100644 index 0000000000..ee3fe2cb2f --- /dev/null +++ b/django/contrib/admindocs/middleware.py @@ -0,0 +1,23 @@ +from django.conf import settings +from django import http + +class XViewMiddleware(object): + """ + Adds an X-View header to internal HEAD requests -- used by the documentation system. + """ + def process_view(self, request, view_func, view_args, view_kwargs): + """ + If the request method is HEAD and either the IP is internal or the + user is a logged-in staff member, quickly return with an x-header + indicating the view function. This is used by the documentation module + to lookup the view function for an arbitrary page. + """ + assert hasattr(request, 'user'), ( + "The XView middleware requires authentication middleware to be " + "installed. Edit your MIDDLEWARE_CLASSES setting to insert " + "'django.contrib.auth.middleware.AuthenticationMiddleware'.") + if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or + (request.user.is_active and request.user.is_staff)): + response = http.HttpResponse() + response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) + return response diff --git a/django/contrib/admindocs/templates/admin_doc/index.html b/django/contrib/admindocs/templates/admin_doc/index.html index 5347341dc0..2af43d78c8 100644 --- a/django/contrib/admindocs/templates/admin_doc/index.html +++ b/django/contrib/admindocs/templates/admin_doc/index.html @@ -7,27 +7,27 @@ › {% trans 'Documentation' %}
{% endblock %} -{% block title %}Documentation{% endblock %} +{% block title %}{% trans 'Documentation' %}{% endblock %} {% block content %} -

Documentation

+

{% trans 'Documentation' %}

-

Tags

-

List of all the template tags and their functions.

+

{% trans 'Tags' %}

+

{% trans 'List of all the template tags and their functions.' %}

-

Filters

-

Filters are actions which can be applied to variables in a template to alter the output.

+

{% trans 'Filters' %}

+

{% trans 'Filters are actions which can be applied to variables in a template to alter the output.' %}

-

Models

-

Models are descriptions of all the objects in the system and their associated fields. Each model has a list of fields which can be accessed as template variables.

+

{% trans 'Models' %}

+

{% trans 'Models are descriptions of all the objects in the system and their associated fields. Each model has a list of fields which can be accessed as template variables' %}.

-

Views

-

Each page on the public site is generated by a view. The view defines which template is used to generate the page and which objects are available to that template.

+

{% trans 'Views' %}

+

{% trans 'Each page on the public site is generated by a view. The view defines which template is used to generate the page and which objects are available to that template.' %}

-

Bookmarklets

-

Tools for your browser to quickly access admin functionality.

+

{% trans 'Bookmarklets' %}

+

{% trans 'Tools for your browser to quickly access admin functionality.' %}

{% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/missing_docutils.html b/django/contrib/admindocs/templates/admin_doc/missing_docutils.html index f8a68ce04c..bd790f779a 100644 --- a/django/contrib/admindocs/templates/admin_doc/missing_docutils.html +++ b/django/contrib/admindocs/templates/admin_doc/missing_docutils.html @@ -7,16 +7,16 @@ › {% trans 'Documentation' %}
{% endblock %} -{% block title %}Please install docutils{% endblock %} +{% block title %}{% trans 'Please install docutils' %}{% endblock %} {% block content %} -

Documentation

+

{% trans 'Documentation' %}

-

The admin documentation system requires Python's docutils library.

+

{% blocktrans with "http://docutils.sf.net/" as link %}The admin documentation system requires Python's docutils library.{% endblocktrans %}

-

Please ask your administrators to install docutils.

+

{% blocktrans with "http://docutils.sf.net/" as link %}Please ask your administrators to install docutils.{% endblocktrans %}

{% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/model_detail.html b/django/contrib/admindocs/templates/admin_doc/model_detail.html index 9fb4eeea14..c1e2bf1e22 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_detail.html +++ b/django/contrib/admindocs/templates/admin_doc/model_detail.html @@ -18,7 +18,7 @@ {% endblock %} -{% block title %}Model: {{ name }}{% endblock %} +{% block title %}{% blocktrans %}Model: {{ name }}{% endblocktrans %}{% endblock %} {% block content %}
@@ -32,9 +32,9 @@ - - - + + + @@ -49,6 +49,6 @@
FieldTypeDescription{% trans 'Field' %}{% trans 'Type' %}{% trans 'Description' %}
-

‹ Back to Models Documentation

+

‹ {% trans 'Back to Models Documentation' %}

{% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/model_index.html b/django/contrib/admindocs/templates/admin_doc/model_index.html index 7a8c69953e..d4cde8334f 100644 --- a/django/contrib/admindocs/templates/admin_doc/model_index.html +++ b/django/contrib/admindocs/templates/admin_doc/model_index.html @@ -11,11 +11,11 @@ {% endblock %} -{% block title %}Models{% endblock %} +{% block title %}{% trans 'Models' %}{% endblock %} {% block content %} -

Model documentation

+

{% trans 'Model documentation' %}

{% regroup models by app_label as grouped_models %} @@ -40,7 +40,7 @@ {% block sidebar %} {% endblock %} -{% block title %}Template filters{% endblock %} +{% block title %}{% trans 'Template filters' %}{% endblock %} {% block content %} -

Template filter documentation

+

{% trans 'Template filter documentation' %}

{% regroup filters|dictsort:"library" by library as filter_libraries %} {% for library in filter_libraries %}
-

{% firstof library.grouper "Built-in filters" %}

- {% if library.grouper %}

To use these filters, put {% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %} in your template before using the filter.


{% endif %} +

{% firstof library.grouper _("Built-in filters") %}

+ {% if library.grouper %}

{% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these filters, put {{ code }} in your template before using the filter.{% endblocktrans %}


{% endif %} {% for filter in library.list|dictsort:"name" %}

{{ filter.name }}

{{ filter.title }} @@ -40,7 +40,7 @@ {% regroup filters|dictsort:"library" by library as filter_libraries %} {% for library in filter_libraries %}
-

{% firstof library.grouper "Built-in filters" %}

+

{% firstof library.grouper _("Built-in filters") %}

    {% for filter in library.list|dictsort:"name" %}
  • {{ filter.name }}
  • diff --git a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html index c0fb243a99..a3c6eaadf4 100644 --- a/django/contrib/admindocs/templates/admin_doc/template_tag_index.html +++ b/django/contrib/admindocs/templates/admin_doc/template_tag_index.html @@ -9,18 +9,18 @@ › {% trans 'Tags' %}
{% endblock %} -{% block title %}Template tags{% endblock %} +{% block title %}{% trans 'Template tags' %}{% endblock %} {% block content %} -

Template tag documentation

+

{% trans 'Template tag documentation' %}

{% regroup tags|dictsort:"library" by library as tag_libraries %} {% for library in tag_libraries %}
-

{% firstof library.grouper "Built-in tags" %}

- {% if library.grouper %}

To use these tags, put {% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %} in your template before using the tag.


{% endif %} +

{% firstof library.grouper _("Built-in tags") %}

+ {% if library.grouper %}

{% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these tags, put {{ code }} in your template before using the tag.{% endblocktrans %}


{% endif %} {% for tag in library.list|dictsort:"name" %}

{{ tag.name }}

{{ tag.title|striptags }}

@@ -40,7 +40,7 @@ {% regroup tags|dictsort:"library" by library as tag_libraries %} {% for library in tag_libraries %}
-

{% firstof library.grouper "Built-in tags" %}

+

{% firstof library.grouper _("Built-in tags") %}

    {% for tag in library.list|dictsort:"name" %}
  • {{ tag.name }}
  • diff --git a/django/contrib/admindocs/templates/admin_doc/view_detail.html b/django/contrib/admindocs/templates/admin_doc/view_detail.html index efe5fed9ed..050e6c800b 100644 --- a/django/contrib/admindocs/templates/admin_doc/view_detail.html +++ b/django/contrib/admindocs/templates/admin_doc/view_detail.html @@ -9,7 +9,7 @@ › {{ name }}
{% endblock %} -{% block title %}View: {{ name }}{% endblock %} +{% block title %}{% blocktrans %}View: {{ name }}{% endblocktrans %}{% endblock %} {% block content %} @@ -20,14 +20,14 @@ {{ body }} {% if meta.Context %} -

Context:

+

{% trans 'Context:' %}

{{ meta.Context }}

{% endif %} {% if meta.Templates %} -

Templates:

+

{% trans 'Templates:' %}

{{ meta.Templates }}

{% endif %} -

‹ Back to Views Documentation

+

‹ {% trans 'Back to Views Documentation' %}

{% endblock %} diff --git a/django/contrib/admindocs/templates/admin_doc/view_index.html b/django/contrib/admindocs/templates/admin_doc/view_index.html index 86342c6dd4..891eee7eec 100644 --- a/django/contrib/admindocs/templates/admin_doc/view_index.html +++ b/django/contrib/admindocs/templates/admin_doc/view_index.html @@ -9,17 +9,17 @@ › {% trans 'Views' %}
{% endblock %} -{% block title %}Views{% endblock %} +{% block title %}{% trans 'Views' %}{% endblock %} {% block content %} -

View documentation

+

{% trans 'View documentation' %}

{% regroup views|dictsort:"site_id" by site as views_by_site %}