Merge remote-tracking branch 'core/master' into schema-alteration
Conflicts: django/db/models/fields/related.py
This commit is contained in:
commit
3c296382b8
33
AUTHORS
33
AUTHORS
|
@ -12,18 +12,25 @@ The PRIMARY AUTHORS are (and/or have been):
|
|||
* Luke Plant
|
||||
* Russell Keith-Magee
|
||||
* Robert Wittams
|
||||
* James Bennett
|
||||
* Gary Wilson
|
||||
* Matt Boersma
|
||||
* Ian Kelly
|
||||
* Joseph Kocherhans
|
||||
* Brian Rosner
|
||||
* Justin Bronn
|
||||
* Karen Tracey
|
||||
* Jannis Leidel
|
||||
* James Tauber
|
||||
* Alex Gaynor
|
||||
* Simon Meers
|
||||
* Andrew Godwin
|
||||
* Carl Meyer
|
||||
* Ramiro Morales
|
||||
* Gabriel Hurley
|
||||
* Chris Beaven
|
||||
* Honza Král
|
||||
* Tim Graham
|
||||
* Idan Gazit
|
||||
* Paul McMillan
|
||||
* Julien Phalip
|
||||
|
@ -36,6 +43,7 @@ The PRIMARY AUTHORS are (and/or have been):
|
|||
* Preston Holmes
|
||||
* Simon Charette
|
||||
* Donald Stufft
|
||||
* Daniel Lindsley
|
||||
* Marc Tamlyn
|
||||
|
||||
More information on the main contributors to Django can be found in
|
||||
|
@ -84,14 +92,15 @@ answer newbie questions, and generally made Django that much better:
|
|||
Randy Barlow <randy@electronsweatshop.com>
|
||||
Scott Barr <scott@divisionbyzero.com.au>
|
||||
Jiri Barton
|
||||
Jorge Bastida <me@jorgebastida.com>
|
||||
Ned Batchelder <http://www.nedbatchelder.com/>
|
||||
batiste@dosimple.ch
|
||||
Batman
|
||||
Brian Beck <http://blog.brianbeck.com/>
|
||||
Shannon -jj Behrens <http://jjinux.blogspot.com/>
|
||||
Esdras Beleza <linux@esdrasbeleza.com>
|
||||
Božidar Benko <bbenko@gmail.com>
|
||||
Chris Bennett <chrisrbennett@yahoo.com>
|
||||
James Bennett
|
||||
Danilo Bargen
|
||||
Shai Berger <shai@platonix.com>
|
||||
berto
|
||||
|
@ -102,9 +111,9 @@ answer newbie questions, and generally made Django that much better:
|
|||
Paul Bissex <http://e-scribe.com/>
|
||||
Loïc Bistuer <loic.bistuer@sixmedia.com>
|
||||
Simon Blanchard
|
||||
Jérémie Blaser <blaserje@gmail.com>
|
||||
Craig Blaszczyk <masterjakul@gmail.com>
|
||||
David Blewett <david@dawninglight.net>
|
||||
Matt Boersma <matt@sprout.org>
|
||||
Artem Gnilov <boobsd@gmail.com>
|
||||
Matías Bordese
|
||||
Nate Bragg <jonathan.bragg@alum.rpi.edu>
|
||||
|
@ -117,6 +126,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
bthomas
|
||||
btoll@bestweb.net
|
||||
Jonathan Buchanan <jonathan.buchanan@gmail.com>
|
||||
Jacob Burch <jacobburch@gmail.com>
|
||||
Keith Bussell <kbussell@gmail.com>
|
||||
C8E
|
||||
Chris Cahoon <chris.cahoon@gmail.com>
|
||||
|
@ -149,6 +159,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Paul Collins <paul.collins.iii@gmail.com>
|
||||
Robert Coup
|
||||
Deric Crago <deric.crago@gmail.com>
|
||||
Brian Fabian Crain <http://www.bfc.do/>
|
||||
David Cramer <dcramer@gmail.com>
|
||||
Pete Crosier <pete.crosier@gmail.com>
|
||||
Matt Croydon <http://www.postneo.com/>
|
||||
|
@ -156,6 +167,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Leah Culver <leah.culver@gmail.com>
|
||||
Raúl Cumplido <raulcumplido@gmail.com>
|
||||
flavio.curella@gmail.com
|
||||
Tome Cvitan <tome@cvitan.com>
|
||||
John D'Agostino <john.dagostino@gmail.com>
|
||||
dackze+django@gmail.com
|
||||
Jim Dalton <jim.dalton@gmail.com>
|
||||
|
@ -188,6 +200,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
J. Clifford Dyer <jcd@sdf.lonestar.org>
|
||||
Clint Ecker
|
||||
Nick Efford <nick@efford.org>
|
||||
Marc Egli <frog32@me.com>
|
||||
eibaan@gmail.com
|
||||
David Eklund
|
||||
Julia Elman
|
||||
|
@ -212,6 +225,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Stefane Fermgier <sf@fermigier.com>
|
||||
J. Pablo Fernandez <pupeno@pupeno.com>
|
||||
Maciej Fijalkowski
|
||||
Leandra Finger <leandra.finger@gmail.com>
|
||||
Juan Pedro Fisanotti <fisadev@gmail.com>
|
||||
Ben Firshman <ben@firshman.co.uk>
|
||||
Matthew Flanagan <http://wadofstuff.blogspot.com>
|
||||
|
@ -239,6 +253,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
pradeep.gowda@gmail.com
|
||||
Collin Grady <collin@collingrady.com>
|
||||
Gabriel Grant <g@briel.ca>
|
||||
Martin Green
|
||||
Daniel Greenfeld
|
||||
Simon Greenhill <dev@simon.net.nz>
|
||||
Owen Griffiths
|
||||
|
@ -268,6 +283,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Eric Holscher <http://ericholscher.com>
|
||||
Ian Holsman <http://feh.holsman.net/>
|
||||
Kieran Holland <http://www.kieranholland.com>
|
||||
Markus Holtermann <http://markusholtermann.eu>
|
||||
Sung-Jin Hong <serialx.net@gmail.com>
|
||||
Leo "hylje" Honkanen <sealage@gmail.com>
|
||||
Matt Hoskins <skaffenuk@googlemail.com>
|
||||
|
@ -278,7 +294,6 @@ answer newbie questions, and generally made Django that much better:
|
|||
Rob Hudson <http://rob.cogit8.org/>
|
||||
Jason Huggins <http://www.jrandolph.com/blog/>
|
||||
Jeff Hui <jeffkhui@gmail.com>
|
||||
Gabriel Hurley <gabriel@strikeawe.com>
|
||||
Hyun Mi Ae
|
||||
Ibon <ibonso@gmail.com>
|
||||
Tom Insam
|
||||
|
@ -327,12 +342,12 @@ answer newbie questions, and generally made Django that much better:
|
|||
Meir Kriheli <http://mksoft.co.il/>
|
||||
Bruce Kroeze <http://coderseye.com/>
|
||||
krzysiek.pawlik@silvermedia.pl
|
||||
Joseph Kocherhans
|
||||
konrad@gwu.edu
|
||||
knox <christobzr@gmail.com>
|
||||
David Krauth
|
||||
Kevin Kubasik <kevin@kubasik.net>
|
||||
kurtiss@meetro.com
|
||||
Vladimir Kuzma <vladimirkuzma.ch@gmail.com>
|
||||
Denis Kuzmichyov <kuzmichyov@gmail.com>
|
||||
Panos Laganakos <panos.laganakos@gmail.com>
|
||||
Nick Lane <nick.lane.au@gmail.com>
|
||||
|
@ -360,7 +375,6 @@ answer newbie questions, and generally made Django that much better:
|
|||
limodou
|
||||
Philip Lindborg <philip.lindborg@gmail.com>
|
||||
Simon Litchfield <simon@quo.com.au>
|
||||
Daniel Lindsley <daniel@toastdriven.com>
|
||||
Trey Long <trey@ktrl.com>
|
||||
Laurent Luce <http://www.laurentluce.com>
|
||||
Martin Mahner <http://www.mahner.org/>
|
||||
|
@ -399,6 +413,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Slawek Mikula <slawek dot mikula at gmail dot com>
|
||||
Katie Miller <katie@sub50.com>
|
||||
Shawn Milochik <shawn@milochik.com>
|
||||
Baptiste Mispelon <bmispelon@gmail.com>
|
||||
mitakummaa@gmail.com
|
||||
Taylor Mitchell <taylor.mitchell@gmail.com>
|
||||
mmarshall
|
||||
|
@ -458,6 +473,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
|
||||
Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
|
||||
Johann Queuniet <johann.queuniet@adh.naellia.eu>
|
||||
Ram Rachum <ram@rachum.com>
|
||||
Jan Rademaker
|
||||
Michael Radziej <mir@noris.de>
|
||||
Laurent Rahuel <laurent.rahuel@gmail.com>
|
||||
|
@ -465,6 +481,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Luciano Ramalho
|
||||
Amit Ramon <amit.ramon@gmail.com>
|
||||
Philippe Raoult <philippe.raoult@n2nsoft.com>
|
||||
Senko Rašić <senko.rasic@dobarkod.hr>
|
||||
Massimiliano Ravelli <massimiliano.ravelli@gmail.com>
|
||||
Brian Ray <http://brianray.chipy.org/>
|
||||
Lee Reilly <lee@leereilly.net>
|
||||
|
@ -480,6 +497,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Alex Robbins <alexander.j.robbins@gmail.com>
|
||||
Matt Robenolt <m@robenolt.com>
|
||||
Henrique Romano <onaiort@gmail.com>
|
||||
Erik Romijn <django@solidlinks.nl>
|
||||
Armin Ronacher
|
||||
Daniel Roseman <http://roseman.org.uk/>
|
||||
Rozza <ross.lawley@gmail.com>
|
||||
|
@ -499,6 +517,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Bernd Schlapsi
|
||||
schwank@gmail.com
|
||||
scott@staplefish.com
|
||||
Olivier Sels <olivier.sels@gmail.com>
|
||||
Ilya Semenov <semenov@inetss.com>
|
||||
Aleksandra Sendecka <asendecka@hauru.eu>
|
||||
serbaut@gmail.com
|
||||
|
@ -523,11 +542,13 @@ answer newbie questions, and generally made Django that much better:
|
|||
Don Spaulding <donspauldingii@gmail.com>
|
||||
Calvin Spealman <ironfroggy@gmail.com>
|
||||
Dane Springmeyer
|
||||
Silvan Spross <silvan.spross@gmail.com>
|
||||
Bjørn Stabell <bjorn@exoweb.net>
|
||||
Georgi Stanojevski <glisha@gmail.com>
|
||||
starrynight <cmorgh@gmail.com>
|
||||
Vasiliy Stavenko <stavenko@gmail.com>
|
||||
Thomas Steinacher <http://www.eggdrop.ch/>
|
||||
Emil Stenström <em@kth.se>
|
||||
Johan C. Stöver <johan@nilling.nl>
|
||||
Nowell Strite <http://nowell.strite.org/>
|
||||
Thomas Stromberg <tstromberg@google.com>
|
||||
|
@ -573,12 +594,14 @@ answer newbie questions, and generally made Django that much better:
|
|||
I.S. van Oostveen <v.oostveen@idca.nl>
|
||||
viestards.lists@gmail.com
|
||||
George Vilches <gav@thataddress.com>
|
||||
Simeon Visser <http://simeonvisser.com>
|
||||
Vlado <vlado@labath.org>
|
||||
Zachary Voase <zacharyvoase@gmail.com>
|
||||
Marijn Vriens <marijn@metronomo.cl>
|
||||
Milton Waddams
|
||||
Chris Wagner <cw264701@ohio.edu>
|
||||
Rick Wagner <rwagner@physics.ucsd.edu>
|
||||
Gavin Wahl <gavinwahl@gmail.com>
|
||||
wam-djangobug@wamber.net
|
||||
Wang Chun <wangchun@exoweb.net>
|
||||
Filip Wasilewski <filip.wasilewski@gmail.com>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -4,7 +4,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Django\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-05-02 16:17+0200\n"
|
||||
"POT-Creation-Date: 2013-05-25 14:27+0200\n"
|
||||
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||
"Last-Translator: Django team\n"
|
||||
"Language-Team: English <en@li.org>\n"
|
||||
|
@ -337,7 +337,7 @@ msgstr ""
|
|||
msgid "Enter a valid value."
|
||||
msgstr ""
|
||||
|
||||
#: core/validators.py:53 forms/fields.py:640
|
||||
#: core/validators.py:53 forms/fields.py:639
|
||||
msgid "Enter a valid URL."
|
||||
msgstr ""
|
||||
|
||||
|
@ -362,7 +362,7 @@ msgstr ""
|
|||
msgid "Enter a valid IPv4 or IPv6 address."
|
||||
msgstr ""
|
||||
|
||||
#: core/validators.py:175 db/models/fields/__init__.py:704
|
||||
#: core/validators.py:175 db/models/fields/__init__.py:706
|
||||
msgid "Enter only digits separated by commas."
|
||||
msgstr ""
|
||||
|
||||
|
@ -408,7 +408,7 @@ msgstr[1] ""
|
|||
msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/base.py:905 forms/models.py:605
|
||||
#: db/models/base.py:905 forms/models.py:643
|
||||
msgid "and"
|
||||
msgstr ""
|
||||
|
||||
|
@ -435,156 +435,156 @@ msgstr ""
|
|||
msgid "Field of type: %(field_type)s"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:568 db/models/fields/__init__.py:1034
|
||||
#: db/models/fields/__init__.py:570 db/models/fields/__init__.py:1036
|
||||
msgid "Integer"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:572 db/models/fields/__init__.py:1032
|
||||
#: db/models/fields/__init__.py:574 db/models/fields/__init__.py:1034
|
||||
#, python-format
|
||||
msgid "'%s' value must be an integer."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:620
|
||||
#: db/models/fields/__init__.py:622
|
||||
#, python-format
|
||||
msgid "'%s' value must be either True or False."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:622
|
||||
#: db/models/fields/__init__.py:624
|
||||
msgid "Boolean (Either True or False)"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:671
|
||||
#: db/models/fields/__init__.py:673
|
||||
#, python-format
|
||||
msgid "String (up to %(max_length)s)"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:699
|
||||
#: db/models/fields/__init__.py:701
|
||||
msgid "Comma-separated integers"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:713
|
||||
#: db/models/fields/__init__.py:715
|
||||
#, python-format
|
||||
msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:715 db/models/fields/__init__.py:803
|
||||
#: db/models/fields/__init__.py:717 db/models/fields/__init__.py:805
|
||||
#, python-format
|
||||
msgid ""
|
||||
"'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:718
|
||||
#: db/models/fields/__init__.py:720
|
||||
msgid "Date (without time)"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:801
|
||||
#: db/models/fields/__init__.py:803
|
||||
#, python-format
|
||||
msgid ""
|
||||
"'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[."
|
||||
"uuuuuu]][TZ] format."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:805
|
||||
#: db/models/fields/__init__.py:807
|
||||
#, python-format
|
||||
msgid ""
|
||||
"'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but "
|
||||
"it is an invalid date/time."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:809
|
||||
#: db/models/fields/__init__.py:811
|
||||
msgid "Date (with time)"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:898
|
||||
#: db/models/fields/__init__.py:900
|
||||
#, python-format
|
||||
msgid "'%s' value must be a decimal number."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:900
|
||||
#: db/models/fields/__init__.py:902
|
||||
msgid "Decimal number"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:957
|
||||
#: db/models/fields/__init__.py:959
|
||||
msgid "Email address"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:976
|
||||
#: db/models/fields/__init__.py:978
|
||||
msgid "File path"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1003
|
||||
#: db/models/fields/__init__.py:1005
|
||||
#, python-format
|
||||
msgid "'%s' value must be a float."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1005
|
||||
#: db/models/fields/__init__.py:1007
|
||||
msgid "Floating point number"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1066
|
||||
#: db/models/fields/__init__.py:1068
|
||||
msgid "Big (8 byte) integer"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1080
|
||||
#: db/models/fields/__init__.py:1082
|
||||
msgid "IPv4 address"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1096
|
||||
#: db/models/fields/__init__.py:1098
|
||||
msgid "IP address"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1139
|
||||
#: db/models/fields/__init__.py:1141
|
||||
#, python-format
|
||||
msgid "'%s' value must be either None, True or False."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1141
|
||||
#: db/models/fields/__init__.py:1143
|
||||
msgid "Boolean (Either True, False or None)"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1190
|
||||
#: db/models/fields/__init__.py:1192
|
||||
msgid "Positive integer"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1201
|
||||
#: db/models/fields/__init__.py:1203
|
||||
msgid "Positive small integer"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1212
|
||||
#: db/models/fields/__init__.py:1214
|
||||
#, python-format
|
||||
msgid "Slug (up to %(max_length)s)"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1230
|
||||
#: db/models/fields/__init__.py:1232
|
||||
msgid "Small integer"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1236
|
||||
#: db/models/fields/__init__.py:1238
|
||||
msgid "Text"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1254
|
||||
#: db/models/fields/__init__.py:1256
|
||||
#, python-format
|
||||
msgid ""
|
||||
"'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1256
|
||||
#: db/models/fields/__init__.py:1258
|
||||
#, python-format
|
||||
msgid ""
|
||||
"'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid "
|
||||
"time."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1259
|
||||
#: db/models/fields/__init__.py:1261
|
||||
msgid "Time"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1321
|
||||
#: db/models/fields/__init__.py:1323
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/__init__.py:1338
|
||||
#: db/models/fields/__init__.py:1340
|
||||
msgid "Raw binary data"
|
||||
msgstr ""
|
||||
|
||||
|
@ -596,55 +596,50 @@ msgstr ""
|
|||
msgid "Image"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/related.py:1133
|
||||
#: db/models/fields/related.py:1118
|
||||
#, python-format
|
||||
msgid "Model %(model)s with pk %(pk)r does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/related.py:1135
|
||||
#: db/models/fields/related.py:1120
|
||||
msgid "Foreign Key (type determined by related field)"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/related.py:1272
|
||||
#: db/models/fields/related.py:1257
|
||||
msgid "One-to-one relationship"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/related.py:1339
|
||||
#: db/models/fields/related.py:1324
|
||||
msgid "Many-to-many relationship"
|
||||
msgstr ""
|
||||
|
||||
#: db/models/fields/related.py:1366
|
||||
msgid ""
|
||||
"Hold down \"Control\", or \"Command\" on a Mac, to select more than one."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:56
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:225
|
||||
#: forms/fields.py:227
|
||||
msgid "Enter a whole number."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:266 forms/fields.py:294
|
||||
#: forms/fields.py:268 forms/fields.py:296
|
||||
msgid "Enter a number."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:296
|
||||
#: forms/fields.py:298
|
||||
#, python-format
|
||||
msgid "Ensure that there are no more than %(max)s digit in total."
|
||||
msgid_plural "Ensure that there are no more than %(max)s digits in total."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: forms/fields.py:300
|
||||
#: forms/fields.py:302
|
||||
#, python-format
|
||||
msgid "Ensure that there are no more than %(max)s decimal place."
|
||||
msgid_plural "Ensure that there are no more than %(max)s decimal places."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: forms/fields.py:304
|
||||
#: forms/fields.py:306
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Ensure that there are no more than %(max)s digit before the decimal point."
|
||||
|
@ -653,31 +648,31 @@ msgid_plural ""
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: forms/fields.py:406 forms/fields.py:1058
|
||||
#: forms/fields.py:408 forms/fields.py:1064
|
||||
msgid "Enter a valid date."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:430 forms/fields.py:1059
|
||||
#: forms/fields.py:432 forms/fields.py:1065
|
||||
msgid "Enter a valid time."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:451
|
||||
#: forms/fields.py:454
|
||||
msgid "Enter a valid date/time."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:525
|
||||
#: forms/fields.py:531
|
||||
msgid "No file was submitted. Check the encoding type on the form."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:526
|
||||
#: forms/fields.py:532
|
||||
msgid "No file was submitted."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:527
|
||||
#: forms/fields.py:533
|
||||
msgid "The submitted file is empty."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:529
|
||||
#: forms/fields.py:535
|
||||
#, python-format
|
||||
msgid "Ensure this filename has at most %(max)d character (it has %(length)d)."
|
||||
msgid_plural ""
|
||||
|
@ -685,22 +680,22 @@ msgid_plural ""
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: forms/fields.py:532
|
||||
#: forms/fields.py:538
|
||||
msgid "Please either submit a file or check the clear checkbox, not both."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:593
|
||||
#: forms/fields.py:599
|
||||
msgid ""
|
||||
"Upload a valid image. The file you uploaded was either not an image or a "
|
||||
"corrupted image."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:746 forms/fields.py:824 forms/models.py:1042
|
||||
#: forms/fields.py:749 forms/fields.py:828 forms/models.py:1096
|
||||
#, python-format
|
||||
msgid "Select a valid choice. %(value)s is not one of the available choices."
|
||||
msgstr ""
|
||||
|
||||
#: forms/fields.py:825 forms/fields.py:928 forms/models.py:1041
|
||||
#: forms/fields.py:829 forms/fields.py:933 forms/models.py:1095
|
||||
msgid "Enter a list of values."
|
||||
msgstr ""
|
||||
|
||||
|
@ -709,53 +704,60 @@ msgstr ""
|
|||
msgid "(Hidden field %(name)s) %(error)s"
|
||||
msgstr ""
|
||||
|
||||
#: forms/formsets.py:305
|
||||
#: forms/formsets.py:310
|
||||
#, python-format
|
||||
msgid "Please submit %s or fewer forms."
|
||||
msgstr ""
|
||||
msgid "Please submit %d or fewer forms."
|
||||
msgid_plural "Please submit %d or fewer forms."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: forms/formsets.py:331 forms/formsets.py:333
|
||||
#: forms/formsets.py:337 forms/formsets.py:339
|
||||
msgid "Order"
|
||||
msgstr ""
|
||||
|
||||
#: forms/formsets.py:335
|
||||
#: forms/formsets.py:341
|
||||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:599
|
||||
#: forms/models.py:637
|
||||
#, python-format
|
||||
msgid "Please correct the duplicate data for %(field)s."
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:603
|
||||
#: forms/models.py:641
|
||||
#, python-format
|
||||
msgid "Please correct the duplicate data for %(field)s, which must be unique."
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:609
|
||||
#: forms/models.py:647
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Please correct the duplicate data for %(field_name)s which must be unique "
|
||||
"for the %(lookup)s in %(date_field)s."
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:617
|
||||
#: forms/models.py:655
|
||||
msgid "Please correct the duplicate values below."
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:883
|
||||
#: forms/models.py:937
|
||||
msgid "The inline foreign key did not match the parent instance primary key."
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:947
|
||||
#: forms/models.py:1001
|
||||
msgid "Select a valid choice. That choice is not one of the available choices."
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:1044
|
||||
#: forms/models.py:1098
|
||||
#, python-format
|
||||
msgid "\"%(pk)s\" is not a valid value for a primary key."
|
||||
msgstr ""
|
||||
|
||||
#: forms/models.py:1109
|
||||
msgid ""
|
||||
"Hold down \"Control\", or \"Command\" on a Mac, to select more than one."
|
||||
msgstr ""
|
||||
|
||||
#: forms/util.py:84
|
||||
#, python-format
|
||||
msgid ""
|
||||
|
@ -791,34 +793,34 @@ msgstr ""
|
|||
msgid "yes,no,maybe"
|
||||
msgstr ""
|
||||
|
||||
#: template/defaultfilters.py:813 template/defaultfilters.py:824
|
||||
#: template/defaultfilters.py:813 template/defaultfilters.py:825
|
||||
#, python-format
|
||||
msgid "%(size)d byte"
|
||||
msgid_plural "%(size)d bytes"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: template/defaultfilters.py:826
|
||||
#: template/defaultfilters.py:827
|
||||
#, python-format
|
||||
msgid "%s KB"
|
||||
msgstr ""
|
||||
|
||||
#: template/defaultfilters.py:828
|
||||
#: template/defaultfilters.py:829
|
||||
#, python-format
|
||||
msgid "%s MB"
|
||||
msgstr ""
|
||||
|
||||
#: template/defaultfilters.py:830
|
||||
#: template/defaultfilters.py:831
|
||||
#, python-format
|
||||
msgid "%s GB"
|
||||
msgstr ""
|
||||
|
||||
#: template/defaultfilters.py:832
|
||||
#: template/defaultfilters.py:833
|
||||
#, python-format
|
||||
msgid "%s TB"
|
||||
msgstr ""
|
||||
|
||||
#: template/defaultfilters.py:833
|
||||
#: template/defaultfilters.py:835
|
||||
#, python-format
|
||||
msgid "%s PB"
|
||||
msgstr ""
|
||||
|
@ -1119,6 +1121,16 @@ msgctxt "alt. month"
|
|||
msgid "December"
|
||||
msgstr ""
|
||||
|
||||
#: utils/image.py:105
|
||||
#, python-format
|
||||
msgid "Neither Pillow nor PIL could be imported: %s"
|
||||
msgstr ""
|
||||
|
||||
#: utils/image.py:127
|
||||
#, python-format
|
||||
msgid "The '_imaging' module for the PIL could not be imported: %s"
|
||||
msgstr ""
|
||||
|
||||
#: utils/text.py:70
|
||||
#, python-format
|
||||
msgctxt "String to return when truncating text"
|
||||
|
@ -1130,53 +1142,53 @@ msgid "or"
|
|||
msgstr ""
|
||||
|
||||
#. Translators: This string is used as a separator between list elements
|
||||
#: utils/text.py:242 utils/timesince.py:54
|
||||
#: utils/text.py:242 utils/timesince.py:55
|
||||
msgid ", "
|
||||
msgstr ""
|
||||
|
||||
#: utils/timesince.py:22
|
||||
#: utils/timesince.py:23
|
||||
#, python-format
|
||||
msgid "%d year"
|
||||
msgid_plural "%d years"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: utils/timesince.py:23
|
||||
#: utils/timesince.py:24
|
||||
#, python-format
|
||||
msgid "%d month"
|
||||
msgid_plural "%d months"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: utils/timesince.py:24
|
||||
#: utils/timesince.py:25
|
||||
#, python-format
|
||||
msgid "%d week"
|
||||
msgid_plural "%d weeks"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: utils/timesince.py:25
|
||||
#: utils/timesince.py:26
|
||||
#, python-format
|
||||
msgid "%d day"
|
||||
msgid_plural "%d days"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: utils/timesince.py:26
|
||||
#: utils/timesince.py:27
|
||||
#, python-format
|
||||
msgid "%d hour"
|
||||
msgid_plural "%d hours"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: utils/timesince.py:27
|
||||
#: utils/timesince.py:28
|
||||
#, python-format
|
||||
msgid "%d minute"
|
||||
msgid_plural "%d minutes"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: utils/timesince.py:43
|
||||
#: utils/timesince.py:44
|
||||
msgid "0 minutes"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
|
||||
class DisallowedModelAdminLookup(SuspiciousOperation):
|
||||
"""Invalid filter was passed to admin view via URL querystring"""
|
||||
pass
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@ class AdminField(object):
|
|||
classes.append('required')
|
||||
if not self.is_first:
|
||||
classes.append('inline')
|
||||
attrs = classes and {'class': ' '.join(classes)} or {}
|
||||
attrs = {'class': ' '.join(classes)} if classes else {}
|
||||
return self.field.label_tag(contents=mark_safe(contents), attrs=attrs)
|
||||
|
||||
def errors(self):
|
||||
|
@ -144,7 +144,7 @@ class AdminReadonlyField(object):
|
|||
# {{ field.name }} must be a useful class name to identify the field.
|
||||
# For convenience, store other field-related data here too.
|
||||
if callable(field):
|
||||
class_name = field.__name__ != '<lambda>' and field.__name__ or ''
|
||||
class_name = field.__name__ if field.__name__ != '<lambda>' else ''
|
||||
else:
|
||||
class_name = field
|
||||
self.field = {
|
||||
|
|
|
@ -4,7 +4,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Django\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-05-02 16:18+0200\n"
|
||||
"POT-Creation-Date: 2013-05-25 14:19+0200\n"
|
||||
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||
"Last-Translator: Django team\n"
|
||||
"Language-Team: English <en@li.org>\n"
|
||||
|
@ -18,12 +18,12 @@ msgstr ""
|
|||
msgid "Successfully deleted %(count)d %(items)s."
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:61 options.py:1365
|
||||
#: actions.py:61 options.py:1418
|
||||
#, python-format
|
||||
msgid "Cannot delete %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: actions.py:63 options.py:1367
|
||||
#: actions.py:63 options.py:1420
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -130,157 +130,157 @@ msgstr ""
|
|||
msgid "LogEntry Object"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:163 options.py:192
|
||||
#: options.py:173 options.py:202
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:710
|
||||
#: options.py:763
|
||||
#, python-format
|
||||
msgid "Changed %s."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:710 options.py:720 options.py:1514
|
||||
#: options.py:763 options.py:773 options.py:1570
|
||||
msgid "and"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:715
|
||||
#: options.py:768
|
||||
#, python-format
|
||||
msgid "Added %(name)s \"%(object)s\"."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:719
|
||||
#: options.py:772
|
||||
#, python-format
|
||||
msgid "Changed %(list)s for %(name)s \"%(object)s\"."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:724
|
||||
#: options.py:777
|
||||
#, python-format
|
||||
msgid "Deleted %(name)s \"%(object)s\"."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:728
|
||||
#: options.py:781
|
||||
msgid "No fields changed."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:831 options.py:874
|
||||
#: options.py:884 options.py:927
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(name)s \"%(obj)s\" was added successfully. You may edit it again below."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:849
|
||||
#: options.py:902
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(name)s \"%(obj)s\" was added successfully. You may add another "
|
||||
"%(name)s below."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:853
|
||||
#: options.py:906
|
||||
#, python-format
|
||||
msgid "The %(name)s \"%(obj)s\" was added successfully."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:867
|
||||
#: options.py:920
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(name)s \"%(obj)s\" was changed successfully. You may edit it again "
|
||||
"below."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:881
|
||||
#: options.py:934
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The %(name)s \"%(obj)s\" was changed successfully. You may add another "
|
||||
"%(name)s below."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:887
|
||||
#: options.py:940
|
||||
#, python-format
|
||||
msgid "The %(name)s \"%(obj)s\" was changed successfully."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:965 options.py:1225
|
||||
#: options.py:1018 options.py:1278
|
||||
msgid ""
|
||||
"Items must be selected in order to perform actions on them. No items have "
|
||||
"been changed."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:984
|
||||
#: options.py:1037
|
||||
msgid "No action selected."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1064
|
||||
#: options.py:1117
|
||||
#, python-format
|
||||
msgid "Add %s"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1088 options.py:1333
|
||||
#: options.py:1141 options.py:1386
|
||||
#, python-format
|
||||
msgid "%(name)s object with primary key %(key)r does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1154
|
||||
#: options.py:1207
|
||||
#, python-format
|
||||
msgid "Change %s"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1204
|
||||
#: options.py:1257
|
||||
msgid "Database error"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1267
|
||||
#: options.py:1320
|
||||
#, python-format
|
||||
msgid "%(count)s %(name)s was changed successfully."
|
||||
msgid_plural "%(count)s %(name)s were changed successfully."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: options.py:1294
|
||||
#: options.py:1347
|
||||
#, python-format
|
||||
msgid "%(total_count)s selected"
|
||||
msgid_plural "All %(total_count)s selected"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: options.py:1299
|
||||
#: options.py:1352
|
||||
#, python-format
|
||||
msgid "0 of %(cnt)s selected"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1350
|
||||
#: options.py:1403
|
||||
#, python-format
|
||||
msgid "The %(name)s \"%(obj)s\" was deleted successfully."
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1406
|
||||
#: options.py:1459
|
||||
#, python-format
|
||||
msgid "Change history: %s"
|
||||
msgstr ""
|
||||
|
||||
#. Translators: Model verbose name and instance representation, suitable to be an item in a list
|
||||
#: options.py:1508
|
||||
#: options.py:1564
|
||||
#, python-format
|
||||
msgid "%(class_name)s %(instance)s"
|
||||
msgstr ""
|
||||
|
||||
#: options.py:1515
|
||||
#: options.py:1571
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Deleting %(class_name)s %(instance)s would require deleting the following "
|
||||
"protected related objects: %(related_objects)s"
|
||||
msgstr ""
|
||||
|
||||
#: sites.py:324 tests.py:71 templates/admin/login.html:48
|
||||
#: sites.py:318 tests.py:71 templates/admin/login.html:48
|
||||
#: templates/registration/password_reset_complete.html:19
|
||||
#: views/decorators.py:24
|
||||
msgid "Log in"
|
||||
msgstr ""
|
||||
|
||||
#: sites.py:392
|
||||
#: sites.py:386
|
||||
msgid "Site administration"
|
||||
msgstr ""
|
||||
|
||||
#: sites.py:446
|
||||
#: sites.py:440
|
||||
#, python-format
|
||||
msgid "%s administration"
|
||||
msgstr ""
|
||||
|
@ -429,9 +429,14 @@ msgstr ""
|
|||
#: templates/admin/auth/user/change_password.html:27
|
||||
#: templates/registration/password_change_form.html:20
|
||||
msgid "Please correct the error below."
|
||||
msgid_plural "Please correct the errors below."
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin/change_form.html:44 templates/admin/change_list.html:67
|
||||
#: templates/admin/login.html:17
|
||||
#: templates/admin/auth/user/change_password.html:27
|
||||
#: templates/registration/password_change_form.html:20
|
||||
msgid "Please correct the errors below."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin/change_list.html:58
|
||||
#, python-format
|
||||
|
@ -811,16 +816,16 @@ msgstr ""
|
|||
msgid "All dates"
|
||||
msgstr ""
|
||||
|
||||
#: views/main.py:37
|
||||
#: views/main.py:35
|
||||
msgid "(None)"
|
||||
msgstr ""
|
||||
|
||||
#: views/main.py:86
|
||||
#: views/main.py:84
|
||||
#, python-format
|
||||
msgid "Select %s"
|
||||
msgstr ""
|
||||
|
||||
#: views/main.py:88
|
||||
#: views/main.py:86
|
||||
#, python-format
|
||||
msgid "Select %s to change"
|
||||
msgstr ""
|
||||
|
|
|
@ -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})]
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
|
||||
{% if form.errors %}
|
||||
<p class="errornote">
|
||||
{% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
|
||||
{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
|
||||
{% if errors %}
|
||||
<p class="errornote">
|
||||
{% blocktrans count counter=errors|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
|
||||
{% if errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
|
||||
</p>
|
||||
{{ adminform.form.non_field_errors }}
|
||||
{% endif %}
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
{% endblock %}
|
||||
{% if cl.formset.errors %}
|
||||
<p class="errornote">
|
||||
{% blocktrans count cl.formset.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
|
||||
{% if cl.formset.errors|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
|
||||
</p>
|
||||
{{ cl.formset.non_form_errors }}
|
||||
{% endif %}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<div class="form-row{% if line.fields|length_is:'1' and line.errors %} errors{% endif %}{% for field in line %}{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% endfor %}">
|
||||
{% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %}
|
||||
{% for field in line %}
|
||||
<div{% if not line.fields|length_is:'1' %} class="field-box{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}"{% endif %}>
|
||||
<div{% if not line.fields|length_is:'1' %} class="field-box{% if field.field.name %} field-{{ field.field.name }}{% endif %}{% if not field.is_readonly and field.errors %} errors{% endif %}"{% elif field.is_checkbox %} class="checkbox-row"{% endif %}>
|
||||
{% if not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %}
|
||||
{% if field.is_checkbox %}
|
||||
{{ field.field }}{{ field.label_tag }}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
{% block content %}
|
||||
{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %}
|
||||
<p class="errornote">
|
||||
{% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
|
||||
{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<div>
|
||||
{% if form.errors %}
|
||||
<p class="errornote">
|
||||
{% blocktrans count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
|
||||
{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -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 }}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -37,9 +37,9 @@ def prepare_lookup_value(key, value):
|
|||
# if key ends with __in, split parameter into separate values
|
||||
if key.endswith('__in'):
|
||||
value = value.split(',')
|
||||
# if key ends with __isnull, special case '' and false
|
||||
# if key ends with __isnull, special case '' and the string literals 'false' and '0'
|
||||
if key.endswith('__isnull'):
|
||||
if value.lower() in ('', 'false'):
|
||||
if value.lower() in ('', 'false', '0'):
|
||||
value = False
|
||||
else:
|
||||
value = True
|
||||
|
@ -269,8 +269,9 @@ def lookup_field(name, obj, model_admin=None):
|
|||
|
||||
def label_for_field(name, model, model_admin=None, return_attr=False):
|
||||
"""
|
||||
Returns a sensible label for a field name. The name can be a callable or the
|
||||
name of an object attributes, as well as a genuine fields. If return_attr is
|
||||
Returns a sensible label for a field name. The name can be a callable,
|
||||
property (but not created with @property decorator) or the name of an
|
||||
object's attribute, as well as a genuine fields. If return_attr is
|
||||
True, the resolved attribute (which could be a callable) is also returned.
|
||||
This will be None if (and only if) the name refers to a field.
|
||||
"""
|
||||
|
@ -303,6 +304,10 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
|
|||
|
||||
if hasattr(attr, "short_description"):
|
||||
label = attr.short_description
|
||||
elif (isinstance(attr, property) and
|
||||
hasattr(attr, "fget") and
|
||||
hasattr(attr.fget, "short_description")):
|
||||
label = attr.fget.short_description
|
||||
elif callable(attr):
|
||||
if attr.__name__ == "<lambda>":
|
||||
label = "--"
|
||||
|
@ -315,6 +320,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
|
|||
else:
|
||||
return label
|
||||
|
||||
|
||||
def help_text_for_field(name, model):
|
||||
try:
|
||||
help_text = model._meta.get_field_by_name(name)[0].help_text
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -4,7 +4,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Django\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-05-02 16:18+0200\n"
|
||||
"POT-Creation-Date: 2013-06-02 00:30-0400\n"
|
||||
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||
"Last-Translator: Django team\n"
|
||||
"Language-Team: English <en@li.org>\n"
|
||||
|
@ -60,12 +60,13 @@ msgstr ""
|
|||
msgid "number of %s"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:270
|
||||
#. Translators: %s is an object type name
|
||||
#: views.py:271
|
||||
#, python-format
|
||||
msgid "Fields on %s objects"
|
||||
msgid "Attributes on %s objects"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:362
|
||||
#: views.py:363
|
||||
#, python-format
|
||||
msgid "%s does not appear to be a urlpattern object"
|
||||
msgstr ""
|
||||
|
@ -83,7 +84,9 @@ msgid "Home"
|
|||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/bookmarklets.html:7 templates/admin_doc/index.html:7
|
||||
#: templates/admin_doc/index.html.py:10 templates/admin_doc/index.html:14
|
||||
#: templates/admin_doc/missing_docutils.html:7
|
||||
#: templates/admin_doc/missing_docutils.html:14
|
||||
#: templates/admin_doc/model_detail.html:15
|
||||
#: templates/admin_doc/model_index.html:9
|
||||
#: templates/admin_doc/template_detail.html:7
|
||||
|
@ -94,7 +97,7 @@ msgstr ""
|
|||
msgid "Documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/bookmarklets.html:8
|
||||
#: templates/admin_doc/bookmarklets.html:8 templates/admin_doc/index.html:29
|
||||
msgid "Bookmarklets"
|
||||
msgstr ""
|
||||
|
||||
|
@ -149,26 +152,202 @@ msgstr ""
|
|||
msgid "As above, but opens the admin page in a new window."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_detail.html:16
|
||||
#: templates/admin_doc/index.html:17
|
||||
#: templates/admin_doc/template_tag_index.html:9
|
||||
msgid "Tags"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:18
|
||||
msgid "List of all the template tags and their functions."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:20
|
||||
#: templates/admin_doc/template_filter_index.html:9
|
||||
msgid "Filters"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:21
|
||||
msgid ""
|
||||
"Filters are actions which can be applied to variables in a template to alter "
|
||||
"the output."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:23 templates/admin_doc/model_detail.html:16
|
||||
#: templates/admin_doc/model_index.html:10
|
||||
#: templates/admin_doc/model_index.html:14
|
||||
msgid "Models"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:24
|
||||
msgid ""
|
||||
"Models are descriptions of all the objects in the system and their "
|
||||
"associated fields. Each model has a list of fields which can be accessed as "
|
||||
"template variables"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:26 templates/admin_doc/view_detail.html:8
|
||||
#: templates/admin_doc/view_index.html:9
|
||||
#: templates/admin_doc/view_index.html:12
|
||||
msgid "Views"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:27
|
||||
msgid ""
|
||||
"Each page on the public site is generated by a view. The view defines which "
|
||||
"template is used to generate the page and which objects are available to "
|
||||
"that template."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/index.html:30
|
||||
msgid "Tools for your browser to quickly access admin functionality."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/missing_docutils.html:10
|
||||
msgid "Please install docutils"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/missing_docutils.html:17
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The admin documentation system requires Python's <a href=\"%(link)s"
|
||||
"\">docutils</a> library."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/missing_docutils.html:19
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Please ask your administrators to install <a href=\"%(link)s\">docutils</a>."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_detail.html:21
|
||||
#, python-format
|
||||
msgid "Model: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_detail.html:35
|
||||
msgid "Field"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_detail.html:36
|
||||
msgid "Type"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_detail.html:37
|
||||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_detail.html:52
|
||||
msgid "Back to Models Documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_index.html:18
|
||||
msgid "Model documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/model_index.html:43
|
||||
msgid "Model groups"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_detail.html:8
|
||||
msgid "Templates"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_filter_index.html:9
|
||||
msgid "Filters"
|
||||
#: templates/admin_doc/template_detail.html:13
|
||||
#, python-format
|
||||
msgid "Template: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_tag_index.html:9
|
||||
msgid "Tags"
|
||||
#: templates/admin_doc/template_detail.html:16
|
||||
#, python-format
|
||||
msgid "Template: \"%(name)s\""
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_detail.html:8
|
||||
#: templates/admin_doc/view_index.html:9
|
||||
msgid "Views"
|
||||
#: templates/admin_doc/template_detail.html:20
|
||||
#, python-format
|
||||
msgid "Search path for template \"%(name)s\" on %(grouper)s:"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_detail.html:23
|
||||
msgid "(does not exist)"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_detail.html:28
|
||||
msgid "Back to Documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_filter_index.html:12
|
||||
msgid "Template filters"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_filter_index.html:16
|
||||
msgid "Template filter documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_filter_index.html:22
|
||||
#: templates/admin_doc/template_filter_index.html:43
|
||||
msgid "Built-in filters"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_filter_index.html:23
|
||||
#, python-format
|
||||
msgid ""
|
||||
"To use these filters, put <code>%(code)s</code> in your template before "
|
||||
"using the filter."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_tag_index.html:12
|
||||
msgid "Template tags"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_tag_index.html:16
|
||||
msgid "Template tag documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_tag_index.html:22
|
||||
#: templates/admin_doc/template_tag_index.html:43
|
||||
msgid "Built-in tags"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/template_tag_index.html:23
|
||||
#, python-format
|
||||
msgid ""
|
||||
"To use these tags, put <code>%(code)s</code> in your template before using "
|
||||
"the tag."
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_detail.html:12
|
||||
#, python-format
|
||||
msgid "View: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_detail.html:23
|
||||
msgid "Context:"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_detail.html:28
|
||||
msgid "Templates:"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_detail.html:32
|
||||
msgid "Back to Views Documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_index.html:16
|
||||
msgid "View documentation"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_index.html:22
|
||||
msgid "Jump to site"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_index.html:35
|
||||
#, python-format
|
||||
msgid "Views by URL on %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: templates/admin_doc/view_index.html:40
|
||||
#, python-format
|
||||
msgid "View function: %(name)s"
|
||||
msgstr ""
|
||||
|
||||
#: tests/test_fields.py:29
|
||||
|
|
|
@ -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
|
|
@ -7,27 +7,27 @@
|
|||
› {% trans 'Documentation' %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block title %}Documentation{% endblock %}
|
||||
{% block title %}{% trans 'Documentation' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Documentation</h1>
|
||||
<h1>{% trans 'Documentation' %}</h1>
|
||||
|
||||
<div id="content-main">
|
||||
<h3><a href="tags/">Tags</a></h3>
|
||||
<p>List of all the template tags and their functions.</p>
|
||||
<h3><a href="tags/">{% trans 'Tags' %}</a></h3>
|
||||
<p>{% trans 'List of all the template tags and their functions.' %}</p>
|
||||
|
||||
<h3><a href="filters/">Filters</a></h3>
|
||||
<p>Filters are actions which can be applied to variables in a template to alter the output.</p>
|
||||
<h3><a href="filters/">{% trans 'Filters' %}</a></h3>
|
||||
<p>{% trans 'Filters are actions which can be applied to variables in a template to alter the output.' %}</p>
|
||||
|
||||
<h3><a href="models/">Models</a></h3>
|
||||
<p>Models are descriptions of all the objects in the system and their associated fields. Each model has a list of fields which can be accessed as template variables.</p>
|
||||
<h3><a href="models/">{% trans 'Models' %}</a></h3>
|
||||
<p>{% trans 'Models are descriptions of all the objects in the system and their associated fields. Each model has a list of fields which can be accessed as template variables' %}.</p>
|
||||
|
||||
<h3><a href="views/">Views</a></h3>
|
||||
<p>Each page on the public site is generated by a view. The view defines which template is used to generate the page and which objects are available to that template.</p>
|
||||
<h3><a href="views/">{% trans 'Views' %}</a></h3>
|
||||
<p>{% trans 'Each page on the public site is generated by a view. The view defines which template is used to generate the page and which objects are available to that template.' %}</p>
|
||||
|
||||
<h3><a href="bookmarklets/">Bookmarklets</a></h3>
|
||||
<p>Tools for your browser to quickly access admin functionality.</p>
|
||||
<h3><a href="bookmarklets/">{% trans 'Bookmarklets' %}</a></h3>
|
||||
<p>{% trans 'Tools for your browser to quickly access admin functionality.' %}</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,16 +7,16 @@
|
|||
› {% trans 'Documentation' %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block title %}Please install docutils{% endblock %}
|
||||
{% block title %}{% trans 'Please install docutils' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Documentation</h1>
|
||||
<h1>{% trans 'Documentation' %}</h1>
|
||||
|
||||
<div id="content-main">
|
||||
<h3>The admin documentation system requires Python's <a href="http://docutils.sf.net/">docutils</a> library.</h3>
|
||||
<h3>{% blocktrans with "http://docutils.sf.net/" as link %}The admin documentation system requires Python's <a href="{{ link }}">docutils</a> library.{% endblocktrans %}</h3>
|
||||
|
||||
<p>Please ask your administrators to install <a href="http://docutils.sf.net/">docutils</a>.</p>
|
||||
<p>{% blocktrans with "http://docutils.sf.net/" as link %}Please ask your administrators to install <a href="{{ link }}">docutils</a>.{% endblocktrans %}</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Model: {{ name }}{% endblock %}
|
||||
{% block title %}{% blocktrans %}Model: {{ name }}{% endblocktrans %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content-main">
|
||||
|
@ -32,9 +32,9 @@
|
|||
<table class="model">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>{% trans 'Field' %}</th>
|
||||
<th>{% trans 'Type' %}</th>
|
||||
<th>{% trans 'Description' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -49,6 +49,6 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<p class="small"><a href="{% url 'django-admindocs-models-index' %}">‹ Back to Models Documentation</a></p>
|
||||
<p class="small"><a href="{% url 'django-admindocs-models-index' %}">‹ {% trans 'Back to Models Documentation' %}</a></p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -11,11 +11,11 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Models{% endblock %}
|
||||
{% block title %}{% trans 'Models' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Model documentation</h1>
|
||||
<h1>{% trans 'Model documentation' %}</h1>
|
||||
|
||||
{% regroup models by app_label as grouped_models %}
|
||||
|
||||
|
@ -40,7 +40,7 @@
|
|||
{% block sidebar %}
|
||||
<div id="content-related" class="sidebar">
|
||||
<div class="module">
|
||||
<h2>Model groups</h2>
|
||||
<h2>{% trans 'Model groups' %}</h2>
|
||||
<ul>
|
||||
{% regroup models by app_label as grouped_models %}
|
||||
{% for group in grouped_models %}
|
||||
|
|
|
@ -10,20 +10,20 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Template: {{ name }}{% endblock %}
|
||||
{% block title %}{% blocktrans %}Template: {{ name }}{% endblocktrans %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Template: "{{ name }}"</h1>
|
||||
<h1>{% blocktrans %}Template: "{{ name }}"{% endblocktrans %}</h1>
|
||||
|
||||
{% regroup templates|dictsort:"site_id" by site as templates_by_site %}
|
||||
{% for group in templates_by_site %}
|
||||
<h2>Search path for template "{{ name }}" on {{ group.grouper }}:</h2>
|
||||
<h2>{% blocktrans with group.grouper as grouper %}Search path for template "{{ name }}" on {{ grouper }}:{% endblocktrans %}</h2>
|
||||
<ol>
|
||||
{% for template in group.list|dictsort:"order" %}
|
||||
<li><code>{{ template.file }}</code>{% if not template.exists %} <em>(does not exist)</em>{% endif %}</li>
|
||||
<li><code>{{ template.file }}</code>{% if not template.exists %} <em>{% trans '(does not exist)' %}</em>{% endif %}</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% endfor %}
|
||||
|
||||
<p class="small"><a href="{% url 'django-admindocs-docroot' %}">‹ Back to Documentation</a></p>
|
||||
<p class="small"><a href="{% url 'django-admindocs-docroot' %}">‹ {% trans 'Back to Documentation' %}</a></p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,18 +9,18 @@
|
|||
› {% trans 'Filters' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block title %}Template filters{% endblock %}
|
||||
{% block title %}{% trans 'Template filters' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Template filter documentation</h1>
|
||||
<h1>{% trans 'Template filter documentation' %}</h1>
|
||||
|
||||
<div id="content-main">
|
||||
{% regroup filters|dictsort:"library" by library as filter_libraries %}
|
||||
{% for library in filter_libraries %}
|
||||
<div class="module">
|
||||
<h2>{% firstof library.grouper "Built-in filters" %}</h2>
|
||||
{% if library.grouper %}<p class="small quiet">To use these filters, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the filter.</p><hr />{% endif %}
|
||||
<h2>{% firstof library.grouper _("Built-in filters") %}</h2>
|
||||
{% if library.grouper %}<p class="small quiet">{% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these filters, put <code>{{ code }}</code> in your template before using the filter.{% endblocktrans %}</p><hr />{% endif %}
|
||||
{% for filter in library.list|dictsort:"name" %}
|
||||
<h3 id="{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</h3>
|
||||
{{ filter.title }}
|
||||
|
@ -40,7 +40,7 @@
|
|||
{% regroup filters|dictsort:"library" by library as filter_libraries %}
|
||||
{% for library in filter_libraries %}
|
||||
<div class="module">
|
||||
<h2>{% firstof library.grouper "Built-in filters" %}</h2>
|
||||
<h2>{% firstof library.grouper _("Built-in filters") %}</h2>
|
||||
<ul>
|
||||
{% for filter in library.list|dictsort:"name" %}
|
||||
<li><a href="#{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</a></li>
|
||||
|
|
|
@ -9,18 +9,18 @@
|
|||
› {% trans 'Tags' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block title %}Template tags{% endblock %}
|
||||
{% block title %}{% trans 'Template tags' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Template tag documentation</h1>
|
||||
<h1>{% trans 'Template tag documentation' %}</h1>
|
||||
|
||||
<div id="content-main">
|
||||
{% regroup tags|dictsort:"library" by library as tag_libraries %}
|
||||
{% for library in tag_libraries %}
|
||||
<div class="module">
|
||||
<h2>{% firstof library.grouper "Built-in tags" %}</h2>
|
||||
{% if library.grouper %}<p class="small quiet">To use these tags, put <code>{% templatetag openblock %} load {{ library.grouper }} {% templatetag closeblock %}</code> in your template before using the tag.</p><hr />{% endif %}
|
||||
<h2>{% firstof library.grouper _("Built-in tags") %}</h2>
|
||||
{% if library.grouper %}<p class="small quiet">{% blocktrans with code="{"|add:"% load "|add:library.grouper|add:" %"|add:"}" %}To use these tags, put <code>{{ code }}</code> in your template before using the tag.{% endblocktrans %}</p><hr />{% endif %}
|
||||
{% for tag in library.list|dictsort:"name" %}
|
||||
<h3 id="{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</h3>
|
||||
<h4>{{ tag.title|striptags }}</h4>
|
||||
|
@ -40,7 +40,7 @@
|
|||
{% regroup tags|dictsort:"library" by library as tag_libraries %}
|
||||
{% for library in tag_libraries %}
|
||||
<div class="module">
|
||||
<h2>{% firstof library.grouper "Built-in tags" %}</h2>
|
||||
<h2>{% firstof library.grouper _("Built-in tags") %}</h2>
|
||||
<ul>
|
||||
{% for tag in library.list|dictsort:"name" %}
|
||||
<li><a href="#{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</a></li>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
› {{ name }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block title %}View: {{ name }}{% endblock %}
|
||||
{% block title %}{% blocktrans %}View: {{ name }}{% endblocktrans %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
@ -20,14 +20,14 @@
|
|||
{{ body }}
|
||||
|
||||
{% if meta.Context %}
|
||||
<h3>Context:</h3>
|
||||
<h3>{% trans 'Context:' %}</h3>
|
||||
<p>{{ meta.Context }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if meta.Templates %}
|
||||
<h3>Templates:</h3>
|
||||
<h3>{% trans 'Templates:' %}</h3>
|
||||
<p>{{ meta.Templates }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="small"><a href="{% url 'django-admindocs-views-index' %}">‹ Back to Views Documentation</a></p>
|
||||
<p class="small"><a href="{% url 'django-admindocs-views-index' %}">‹ {% trans 'Back to Views Documentation' %}</a></p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -9,17 +9,17 @@
|
|||
› {% trans 'Views' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block title %}Views{% endblock %}
|
||||
{% block title %}{% trans 'Views' %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>View documentation</h1>
|
||||
<h1>{% trans 'View documentation' %}</h1>
|
||||
|
||||
{% regroup views|dictsort:"site_id" by site as views_by_site %}
|
||||
|
||||
<div id="content-related" class="sidebar">
|
||||
<div class="module">
|
||||
<h2>Jump to site</h2>
|
||||
<h2>{% trans 'Jump to site' %}</h2>
|
||||
<ul>
|
||||
{% for site_views in views_by_site %}
|
||||
<li><a href="#site{{ site_views.grouper.id }}">{{ site_views.grouper.name }}</a></li>
|
||||
|
@ -32,12 +32,12 @@
|
|||
|
||||
{% for site_views in views_by_site %}
|
||||
<div class="module">
|
||||
<h2 id="site{{ site_views.grouper.id }}">Views by URL on {{ site_views.grouper.name }}</h2>
|
||||
<h2 id="site{{ site_views.grouper.id }}">{% blocktrans with site_views.grouper.name as name %}Views by URL on {{ name }}{% endblocktrans %}</h2>
|
||||
|
||||
{% for view in site_views.list|dictsort:"url" %}
|
||||
{% ifchanged %}
|
||||
<h3><a href="{% url 'django-admindocs-views-detail' view=view.full_name %}">{{ view.url }}</a></h3>
|
||||
<p class="small quiet">View function: {{ view.full_name }}</p>
|
||||
<p class="small quiet">{% blocktrans with view.full_name as name %}View function: {{ name }}{% endblocktrans %}</p>
|
||||
<p>{{ view.title }}</p>
|
||||
<hr />
|
||||
{% endifchanged %}
|
||||
|
@ -46,5 +46,3 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ def bookmarklets(request):
|
|||
admin_root = urlresolvers.reverse('admin:index')
|
||||
return render_to_response('admin_doc/bookmarklets.html', {
|
||||
'root_path': admin_root,
|
||||
'admin_url': "%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root),
|
||||
'admin_url': "%s://%s%s" % ('https' if request.is_secure() else 'http', request.get_host(), admin_root),
|
||||
}, context_instance=RequestContext(request))
|
||||
|
||||
@staff_member_required
|
||||
|
@ -267,6 +267,7 @@ def model_detail(request, app_label, model_name):
|
|||
return render_to_response('admin_doc/model_detail.html', {
|
||||
'root_path': urlresolvers.reverse('admin:index'),
|
||||
'name': '%s.%s' % (opts.app_label, opts.object_name),
|
||||
# Translators: %s is an object type name
|
||||
'summary': _("Attributes on %s objects") % opts.object_name,
|
||||
'description': model.__doc__,
|
||||
'fields': fields,
|
||||
|
@ -286,7 +287,7 @@ def template_detail(request, template):
|
|||
templates.append({
|
||||
'file': template_file,
|
||||
'exists': os.path.exists(template_file),
|
||||
'contents': lambda: os.path.exists(template_file) and open(template_file).read() or '',
|
||||
'contents': lambda: open(template_file).read() if os.path.exists(template_file) else '',
|
||||
'site_id': settings_mod.SITE_ID,
|
||||
'site': site_obj,
|
||||
'order': list(settings_mod.TEMPLATE_DIRS).index(dir),
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import re
|
||||
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.utils.module_loading import import_by_path
|
||||
from django.middleware.csrf import rotate_token
|
||||
|
||||
from .signals import user_logged_in, user_logged_out, user_login_failed
|
||||
|
||||
SESSION_KEY = '_auth_user_id'
|
||||
BACKEND_SESSION_KEY = '_auth_user_backend'
|
||||
|
@ -14,7 +17,6 @@ def load_backend(path):
|
|||
|
||||
|
||||
def get_backends():
|
||||
from django.conf import settings
|
||||
backends = []
|
||||
for backend_path in settings.AUTHENTICATION_BACKENDS:
|
||||
backends.append(load_backend(backend_path))
|
||||
|
@ -83,6 +85,7 @@ def login(request, user):
|
|||
request.session[BACKEND_SESSION_KEY] = user.backend
|
||||
if hasattr(request, 'user'):
|
||||
request.user = user
|
||||
rotate_token(request)
|
||||
user_logged_in.send(sender=user.__class__, request=request, user=user)
|
||||
|
||||
|
||||
|
@ -106,7 +109,6 @@ def logout(request):
|
|||
|
||||
def get_user_model():
|
||||
"Return the User model that is active in this project"
|
||||
from django.conf import settings
|
||||
from django.db.models import get_model
|
||||
|
||||
try:
|
||||
|
@ -120,12 +122,13 @@ def get_user_model():
|
|||
|
||||
|
||||
def get_user(request):
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from .models import AnonymousUser
|
||||
try:
|
||||
user_id = request.session[SESSION_KEY]
|
||||
backend_path = request.session[BACKEND_SESSION_KEY]
|
||||
assert backend_path in settings.AUTHENTICATION_BACKENDS
|
||||
backend = load_backend(backend_path)
|
||||
user = backend.get_user(user_id) or AnonymousUser()
|
||||
except KeyError:
|
||||
except (KeyError, AssertionError):
|
||||
user = AnonymousUser()
|
||||
return user
|
||||
|
|
|
@ -237,7 +237,7 @@ class PasswordResetForm(forms.Form):
|
|||
'uid': int_to_base36(user.pk),
|
||||
'user': user,
|
||||
'token': token_generator.make_token(user),
|
||||
'protocol': use_https and 'https' or 'http',
|
||||
'protocol': 'https' if use_https else 'http',
|
||||
}
|
||||
subject = loader.render_to_string(subject_template_name, c)
|
||||
# Email subject *must not* contain newlines
|
||||
|
|
|
@ -80,7 +80,7 @@ def create_permissions(app, created_models, verbosity, db=DEFAULT_DB_ALIAS, **kw
|
|||
for perm in _get_all_permissions(klass._meta, ctype):
|
||||
searched_perms.append((ctype, perm))
|
||||
|
||||
# Find all the Permissions that have a context_type for a model we're
|
||||
# Find all the Permissions that have a content_type for a model we're
|
||||
# looking for. We don't need to check for codenames since we already have
|
||||
# a list of the ones we're going to create.
|
||||
all_perms = set(auth_app.Permission.objects.using(db).filter(
|
||||
|
|
|
@ -177,7 +177,7 @@ class UserManager(BaseUserManager):
|
|||
now = timezone.now()
|
||||
if not username:
|
||||
raise ValueError('The given username must be set')
|
||||
email = UserManager.normalize_email(email)
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(username=username, email=email,
|
||||
is_staff=False, is_active=True, is_superuser=False,
|
||||
last_login=now, date_joined=now, **extra_fields)
|
||||
|
|
|
@ -2,12 +2,14 @@ from __future__ import unicode_literals
|
|||
from datetime import date
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import User, Group, Permission, AnonymousUser
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.contrib.auth.tests.test_custom_user import ExtensionUser, CustomPermissionsUser, CustomUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth import authenticate, get_user
|
||||
from django.http import HttpRequest
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
|
@ -402,3 +404,52 @@ class PermissionDeniedBackendTest(TestCase):
|
|||
settings.AUTHENTICATION_BACKENDS) + (backend, ))
|
||||
def test_authenticates(self):
|
||||
self.assertEqual(authenticate(username='test', password='test'), self.user1)
|
||||
|
||||
|
||||
class NewModelBackend(ModelBackend):
|
||||
pass
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
class ChangedBackendSettingsTest(TestCase):
|
||||
"""
|
||||
Tests for changes in the settings.AUTHENTICATION_BACKENDS
|
||||
"""
|
||||
backend = 'django.contrib.auth.tests.test_auth_backends.NewModelBackend'
|
||||
|
||||
TEST_USERNAME = 'test_user'
|
||||
TEST_PASSWORD = 'test_password'
|
||||
TEST_EMAIL = 'test@example.com'
|
||||
|
||||
def setUp(self):
|
||||
User.objects.create_user(self.TEST_USERNAME,
|
||||
self.TEST_EMAIL,
|
||||
self.TEST_PASSWORD)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=(backend, ))
|
||||
def test_changed_backend_settings(self):
|
||||
"""
|
||||
Tests that removing a backend configured in AUTHENTICATION_BACKENDS
|
||||
make already logged-in users disconnect.
|
||||
"""
|
||||
|
||||
# Get a session for the test user
|
||||
self.assertTrue(self.client.login(
|
||||
username=self.TEST_USERNAME,
|
||||
password=self.TEST_PASSWORD)
|
||||
)
|
||||
|
||||
# Prepare a request object
|
||||
request = HttpRequest()
|
||||
request.session = self.client.session
|
||||
|
||||
# Remove NewModelBackend
|
||||
with self.settings(AUTHENTICATION_BACKENDS=(
|
||||
'django.contrib.auth.backends.ModelBackend',)):
|
||||
# Get the user from the request
|
||||
user = get_user(request)
|
||||
|
||||
# Assert that the user retrieval is successful and the user is
|
||||
# anonymous as the backend is not longer available.
|
||||
self.assertIsNotNone(user)
|
||||
self.assertTrue(user.is_anonymous())
|
||||
|
|
|
@ -21,7 +21,7 @@ class CustomUserManager(BaseUserManager):
|
|||
raise ValueError('Users must have an email address')
|
||||
|
||||
user = self.model(
|
||||
email=CustomUserManager.normalize_email(email),
|
||||
email=self.normalize_email(email),
|
||||
date_of_birth=date_of_birth,
|
||||
)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.contrib.auth.management.commands import changepassword
|
|||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.tests.test_custom_user import CustomUser
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.core.management.validation import get_validation_errors
|
||||
|
@ -195,6 +196,7 @@ class PermissionDuplicationTestCase(TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
models.Permission._meta.permissions = self._original_permissions
|
||||
ContentType.objects.clear_cache()
|
||||
|
||||
def test_duplicated_permissions(self):
|
||||
"""
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
import itertools
|
||||
import os
|
||||
import re
|
||||
try:
|
||||
from urllib.parse import urlparse, ParseResult
|
||||
except ImportError: # Python 2
|
||||
from urlparse import urlparse, ParseResult
|
||||
|
||||
from django.conf import global_settings, settings
|
||||
from django.contrib.sites.models import Site, RequestSite
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from django.http import QueryDict
|
||||
from django.http import QueryDict, HttpRequest
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import urlquote
|
||||
from django.utils._os import upath
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.test.utils import override_settings, patch_logger
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
|
||||
from django.contrib.auth import SESSION_KEY, REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm,
|
||||
SetPasswordForm, PasswordResetForm)
|
||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||
from django.contrib.auth.views import login as login_view
|
||||
|
||||
|
||||
@override_settings(
|
||||
|
@ -46,15 +52,31 @@ class AuthViewsTestCase(TestCase):
|
|||
'username': 'testclient',
|
||||
'password': password,
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith(settings.LOGIN_REDIRECT_URL))
|
||||
self.assertTrue(SESSION_KEY in self.client.session)
|
||||
return response
|
||||
|
||||
def assertFormError(self, response, error):
|
||||
"""Assert that error is found in response.context['form'] errors"""
|
||||
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
|
||||
self.assertIn(force_text(error), form_errors)
|
||||
|
||||
def assertURLEqual(self, url, expected, parse_qs=False):
|
||||
"""
|
||||
Given two URLs, make sure all their components (the ones given by
|
||||
urlparse) are equal, only comparing components that are present in both
|
||||
URLs.
|
||||
If `parse_qs` is True, then the querystrings are parsed with QueryDict.
|
||||
This is useful if you don't want the order of parameters to matter.
|
||||
Otherwise, the query strings are compared as-is.
|
||||
"""
|
||||
fields = ParseResult._fields
|
||||
|
||||
for attr, x, y in zip(fields, urlparse(url), urlparse(expected)):
|
||||
if parse_qs and attr == 'query':
|
||||
x, y = QueryDict(x), QueryDict(y)
|
||||
if x and y and x != y:
|
||||
self.fail("%r != %r (%s doesn't match)" % (url, expected, attr))
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
class AuthViewNamedURLTests(AuthViewsTestCase):
|
||||
|
@ -132,28 +154,32 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||
# produce a meaningful reset URL, we need to be certain that the
|
||||
# HTTP_HOST header isn't poisoned. This is done as a check when get_host()
|
||||
# is invoked, but we check here as a practical consequence.
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
self.client.post('/password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||
response = self.client.post('/password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertEqual(len(logger_calls), 1)
|
||||
|
||||
# Skip any 500 handler action (like sending more mail...)
|
||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||
def test_poisoned_http_host_admin_site(self):
|
||||
"Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
self.client.post('/admin_password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||
response = self.client.post('/admin_password_reset/',
|
||||
{'email': 'staffmember@example.com'},
|
||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
self.assertEqual(len(logger_calls), 1)
|
||||
|
||||
|
||||
def _test_confirm_start(self):
|
||||
# Start by creating the email
|
||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 1)
|
||||
return self._read_signup_email(mail.outbox[0])
|
||||
|
||||
|
@ -205,8 +231,6 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||
'new_password2': 'anewpassword'})
|
||||
# It redirects us to a 'complete' page:
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# Check the password has been changed
|
||||
u = User.objects.get(email='staffmember@example.com')
|
||||
self.assertTrue(u.check_password("anewpassword"))
|
||||
|
@ -221,6 +245,47 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||
'new_password2': 'x'})
|
||||
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
|
||||
|
||||
def test_reset_redirect_default(self):
|
||||
response = self.client.post('/password_reset/',
|
||||
{'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/done/')
|
||||
|
||||
def test_reset_custom_redirect(self):
|
||||
response = self.client.post('/password_reset/custom_redirect/',
|
||||
{'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_reset_custom_redirect_named(self):
|
||||
response = self.client.post('/password_reset/custom_redirect/named/',
|
||||
{'email': 'staffmember@example.com'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
def test_confirm_redirect_default(self):
|
||||
url, path = self._test_confirm_start()
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||
'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/reset/done/')
|
||||
|
||||
def test_confirm_redirect_custom(self):
|
||||
url, path = self._test_confirm_start()
|
||||
path = path.replace('/reset/', '/reset/custom/')
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||
'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_confirm_redirect_custom_named(self):
|
||||
url, path = self._test_confirm_start()
|
||||
path = path.replace('/reset/', '/reset/custom/named/')
|
||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||
'new_password2': 'anewpassword'})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
|
||||
@override_settings(AUTH_USER_MODEL='auth.CustomUser')
|
||||
class CustomUserPasswordResetTest(AuthViewsTestCase):
|
||||
|
@ -285,8 +350,6 @@ class ChangePasswordTest(AuthViewsTestCase):
|
|||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/password_change/done/'))
|
||||
self.fail_login()
|
||||
self.login(password='password1')
|
||||
|
||||
|
@ -298,20 +361,50 @@ class ChangePasswordTest(AuthViewsTestCase):
|
|||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/password_change/done/'))
|
||||
self.assertURLEqual(response.url, '/password_change/done/')
|
||||
|
||||
@override_settings(LOGIN_URL='/login/')
|
||||
def test_password_change_done_fails(self):
|
||||
with self.settings(LOGIN_URL='/login/'):
|
||||
response = self.client.get('/password_change/done/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/login/?next=/password_change/done/'))
|
||||
response = self.client.get('/password_change/done/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/login/?next=/password_change/done/')
|
||||
|
||||
def test_password_change_redirect_default(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_change/done/')
|
||||
|
||||
def test_password_change_redirect_custom(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/custom/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/custom/')
|
||||
|
||||
def test_password_change_redirect_custom_named(self):
|
||||
self.login()
|
||||
response = self.client.post('/password_change/custom/named/', {
|
||||
'old_password': 'password',
|
||||
'new_password1': 'password1',
|
||||
'new_password2': 'password1',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
class LoginTest(AuthViewsTestCase):
|
||||
|
||||
def test_current_site_in_context_after_login(self):
|
||||
response = self.client.get(reverse('django.contrib.auth.views.login'))
|
||||
response = self.client.get(reverse('login'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
if Site._meta.installed:
|
||||
site = Site.objects.get_current()
|
||||
|
@ -323,7 +416,7 @@ class LoginTest(AuthViewsTestCase):
|
|||
'Login form is not an AuthenticationForm')
|
||||
|
||||
def test_security_check(self, password='password'):
|
||||
login_url = reverse('django.contrib.auth.views.login')
|
||||
login_url = reverse('login')
|
||||
|
||||
# Those URLs should not pass the security check
|
||||
for bad_url in ('http://example.com',
|
||||
|
@ -374,63 +467,103 @@ class LoginTest(AuthViewsTestCase):
|
|||
# the custom authentication form used by this login asserts
|
||||
# that a request is passed to the form successfully.
|
||||
|
||||
def test_login_csrf_rotate(self, password='password'):
|
||||
"""
|
||||
Makes sure that a login rotates the currently-used CSRF token.
|
||||
"""
|
||||
# Do a GET to establish a CSRF token
|
||||
# TestClient isn't used here as we're testing middleware, essentially.
|
||||
req = HttpRequest()
|
||||
CsrfViewMiddleware().process_view(req, login_view, (), {})
|
||||
req.META["CSRF_COOKIE_USED"] = True
|
||||
resp = login_view(req)
|
||||
resp2 = CsrfViewMiddleware().process_response(req, resp)
|
||||
csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None)
|
||||
token1 = csrf_cookie.coded_value
|
||||
|
||||
# Prepare the POST request
|
||||
req = HttpRequest()
|
||||
req.COOKIES[settings.CSRF_COOKIE_NAME] = token1
|
||||
req.method = "POST"
|
||||
req.POST = {'username': 'testclient', 'password': password, 'csrfmiddlewaretoken': token1}
|
||||
req.REQUEST = req.POST
|
||||
|
||||
# Use POST request to log in
|
||||
SessionMiddleware().process_request(req)
|
||||
CsrfViewMiddleware().process_view(req, login_view, (), {})
|
||||
req.META["SERVER_NAME"] = "testserver" # Required to have redirect work in login view
|
||||
req.META["SERVER_PORT"] = 80
|
||||
req.META["CSRF_COOKIE_USED"] = True
|
||||
resp = login_view(req)
|
||||
resp2 = CsrfViewMiddleware().process_response(req, resp)
|
||||
csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, None)
|
||||
token2 = csrf_cookie.coded_value
|
||||
|
||||
# Check the CSRF token switched
|
||||
self.assertNotEqual(token1, token2)
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
class LoginURLSettings(AuthViewsTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(LoginURLSettings, self).setUp()
|
||||
self.old_LOGIN_URL = settings.LOGIN_URL
|
||||
|
||||
def tearDown(self):
|
||||
super(LoginURLSettings, self).tearDown()
|
||||
settings.LOGIN_URL = self.old_LOGIN_URL
|
||||
|
||||
def get_login_required_url(self, login_url):
|
||||
settings.LOGIN_URL = login_url
|
||||
"""Tests for settings.LOGIN_URL."""
|
||||
def assertLoginURLEquals(self, url, parse_qs=False):
|
||||
response = self.client.get('/login_required/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
return response.url
|
||||
self.assertURLEqual(response.url, url, parse_qs=parse_qs)
|
||||
|
||||
@override_settings(LOGIN_URL='/login/')
|
||||
def test_standard_login_url(self):
|
||||
login_url = '/login/'
|
||||
login_required_url = self.get_login_required_url(login_url)
|
||||
querystring = QueryDict('', mutable=True)
|
||||
querystring['next'] = '/login_required/'
|
||||
self.assertEqual(login_required_url, 'http://testserver%s?%s' %
|
||||
(login_url, querystring.urlencode('/')))
|
||||
self.assertLoginURLEquals('/login/?next=/login_required/')
|
||||
|
||||
@override_settings(LOGIN_URL='login')
|
||||
def test_named_login_url(self):
|
||||
self.assertLoginURLEquals('/login/?next=/login_required/')
|
||||
|
||||
@override_settings(LOGIN_URL='http://remote.example.com/login')
|
||||
def test_remote_login_url(self):
|
||||
login_url = 'http://remote.example.com/login'
|
||||
login_required_url = self.get_login_required_url(login_url)
|
||||
querystring = QueryDict('', mutable=True)
|
||||
querystring['next'] = 'http://testserver/login_required/'
|
||||
self.assertEqual(login_required_url,
|
||||
'%s?%s' % (login_url, querystring.urlencode('/')))
|
||||
quoted_next = urlquote('http://testserver/login_required/')
|
||||
expected = 'http://remote.example.com/login?next=%s' % quoted_next
|
||||
self.assertLoginURLEquals(expected)
|
||||
|
||||
@override_settings(LOGIN_URL='https:///login/')
|
||||
def test_https_login_url(self):
|
||||
login_url = 'https:///login/'
|
||||
login_required_url = self.get_login_required_url(login_url)
|
||||
querystring = QueryDict('', mutable=True)
|
||||
querystring['next'] = 'http://testserver/login_required/'
|
||||
self.assertEqual(login_required_url,
|
||||
'%s?%s' % (login_url, querystring.urlencode('/')))
|
||||
quoted_next = urlquote('http://testserver/login_required/')
|
||||
expected = 'https:///login/?next=%s' % quoted_next
|
||||
self.assertLoginURLEquals(expected)
|
||||
|
||||
@override_settings(LOGIN_URL='/login/?pretty=1')
|
||||
def test_login_url_with_querystring(self):
|
||||
login_url = '/login/?pretty=1'
|
||||
login_required_url = self.get_login_required_url(login_url)
|
||||
querystring = QueryDict('pretty=1', mutable=True)
|
||||
querystring['next'] = '/login_required/'
|
||||
self.assertEqual(login_required_url, 'http://testserver/login/?%s' %
|
||||
querystring.urlencode('/'))
|
||||
self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/', parse_qs=True)
|
||||
|
||||
@override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/')
|
||||
def test_remote_login_url_with_next_querystring(self):
|
||||
login_url = 'http://remote.example.com/login/'
|
||||
login_required_url = self.get_login_required_url('%s?next=/default/' %
|
||||
login_url)
|
||||
querystring = QueryDict('', mutable=True)
|
||||
querystring['next'] = 'http://testserver/login_required/'
|
||||
self.assertEqual(login_required_url, '%s?%s' % (login_url,
|
||||
querystring.urlencode('/')))
|
||||
quoted_next = urlquote('http://testserver/login_required/')
|
||||
expected = 'http://remote.example.com/login/?next=%s' % quoted_next
|
||||
self.assertLoginURLEquals(expected)
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
class LoginRedirectUrlTest(AuthViewsTestCase):
|
||||
"""Tests for settings.LOGIN_REDIRECT_URL."""
|
||||
def assertLoginRedirectURLEqual(self, url):
|
||||
response = self.login()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, url)
|
||||
|
||||
def test_default(self):
|
||||
self.assertLoginRedirectURLEqual('/accounts/profile/')
|
||||
|
||||
@override_settings(LOGIN_REDIRECT_URL='/custom/')
|
||||
def test_custom(self):
|
||||
self.assertLoginRedirectURLEqual('/custom/')
|
||||
|
||||
@override_settings(LOGIN_REDIRECT_URL='password_reset')
|
||||
def test_named(self):
|
||||
self.assertLoginRedirectURLEqual('/password_reset/')
|
||||
|
||||
@override_settings(LOGIN_REDIRECT_URL='http://remote.example.com/welcome/')
|
||||
def test_remote(self):
|
||||
self.assertLoginRedirectURLEqual('http://remote.example.com/welcome/')
|
||||
|
||||
|
||||
@skipIfCustomUser
|
||||
|
@ -457,11 +590,11 @@ class LogoutTest(AuthViewsTestCase):
|
|||
self.login()
|
||||
response = self.client.get('/logout/next_page/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/somewhere/'))
|
||||
self.assertURLEqual(response.url, '/somewhere/')
|
||||
|
||||
response = self.client.get('/logout/next_page/?next=/login/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/login/'))
|
||||
self.assertURLEqual(response.url, '/login/')
|
||||
|
||||
self.confirm_logged_out()
|
||||
|
||||
|
@ -470,7 +603,7 @@ class LogoutTest(AuthViewsTestCase):
|
|||
self.login()
|
||||
response = self.client.get('/logout/next_page/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/somewhere/'))
|
||||
self.assertURLEqual(response.url, '/somewhere/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_logout_with_redirect_argument(self):
|
||||
|
@ -478,7 +611,7 @@ class LogoutTest(AuthViewsTestCase):
|
|||
self.login()
|
||||
response = self.client.get('/logout/?next=/login/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/login/'))
|
||||
self.assertURLEqual(response.url, '/login/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_logout_with_custom_redirect_argument(self):
|
||||
|
@ -486,11 +619,19 @@ class LogoutTest(AuthViewsTestCase):
|
|||
self.login()
|
||||
response = self.client.get('/logout/custom_query/?follow=/somewhere/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertTrue(response.url.endswith('/somewhere/'))
|
||||
self.assertURLEqual(response.url, '/somewhere/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_logout_with_named_redirect(self):
|
||||
"Logout resolves names or URLs passed as next_page."
|
||||
self.login()
|
||||
response = self.client.get('/logout/next_page/named/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertURLEqual(response.url, '/password_reset/')
|
||||
self.confirm_logged_out()
|
||||
|
||||
def test_security_check(self, password='password'):
|
||||
logout_url = reverse('django.contrib.auth.views.logout')
|
||||
logout_url = reverse('logout')
|
||||
|
||||
# Those URLs should not pass the security check
|
||||
for bad_url in ('http://example.com',
|
||||
|
@ -541,5 +682,7 @@ class ChangelistTests(AuthViewsTestCase):
|
|||
self.login()
|
||||
|
||||
# A lookup that tries to filter on password isn't OK
|
||||
with self.assertRaises(SuspiciousOperation):
|
||||
with patch_logger('django.security.DisallowedModelAdminLookup', 'error') as logger_calls:
|
||||
response = self.client.get('/admin/auth/user/?password__startswith=sha1$')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(len(logger_calls), 1)
|
||||
|
|
|
@ -62,8 +62,19 @@ def custom_request_auth_login(request):
|
|||
urlpatterns = urlpatterns + patterns('',
|
||||
(r'^logout/custom_query/$', 'django.contrib.auth.views.logout', dict(redirect_field_name='follow')),
|
||||
(r'^logout/next_page/$', 'django.contrib.auth.views.logout', dict(next_page='/somewhere/')),
|
||||
(r'^logout/next_page/named/$', 'django.contrib.auth.views.logout', dict(next_page='password_reset')),
|
||||
(r'^remote_user/$', remote_user_auth_view),
|
||||
(r'^password_reset_from_email/$', 'django.contrib.auth.views.password_reset', dict(from_email='staffmember@example.com')),
|
||||
(r'^password_reset/custom_redirect/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='/custom/')),
|
||||
(r'^password_reset/custom_redirect/named/$', 'django.contrib.auth.views.password_reset', dict(post_reset_redirect='password_reset')),
|
||||
(r'^reset/custom/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
'django.contrib.auth.views.password_reset_confirm',
|
||||
dict(post_reset_redirect='/custom/')),
|
||||
(r'^reset/custom/named/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
'django.contrib.auth.views.password_reset_confirm',
|
||||
dict(post_reset_redirect='password_reset')),
|
||||
(r'^password_change/custom/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='/custom/')),
|
||||
(r'^password_change/custom/named/$', 'django.contrib.auth.views.password_change', dict(post_change_redirect='password_reset')),
|
||||
(r'^admin_password_reset/$', 'django.contrib.auth.views.password_reset', dict(is_admin_site=True)),
|
||||
(r'^login_required/$', login_required(password_reset)),
|
||||
(r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')),
|
||||
|
|
|
@ -72,6 +72,9 @@ def logout(request, next_page=None,
|
|||
"""
|
||||
auth_logout(request)
|
||||
|
||||
if next_page is not None:
|
||||
next_page = resolve_url(next_page)
|
||||
|
||||
if redirect_field_name in request.REQUEST:
|
||||
next_page = request.REQUEST[redirect_field_name]
|
||||
# Security check -- don't allow redirection to a different host.
|
||||
|
@ -139,7 +142,9 @@ def password_reset(request, is_admin_site=False,
|
|||
current_app=None,
|
||||
extra_context=None):
|
||||
if post_reset_redirect is None:
|
||||
post_reset_redirect = reverse('django.contrib.auth.views.password_reset_done')
|
||||
post_reset_redirect = reverse('password_reset_done')
|
||||
else:
|
||||
post_reset_redirect = resolve_url(post_reset_redirect)
|
||||
if request.method == "POST":
|
||||
form = password_reset_form(request.POST)
|
||||
if form.is_valid():
|
||||
|
@ -192,7 +197,9 @@ def password_reset_confirm(request, uidb36=None, token=None,
|
|||
UserModel = get_user_model()
|
||||
assert uidb36 is not None and token is not None # checked by URLconf
|
||||
if post_reset_redirect is None:
|
||||
post_reset_redirect = reverse('django.contrib.auth.views.password_reset_complete')
|
||||
post_reset_redirect = reverse('password_reset_complete')
|
||||
else:
|
||||
post_reset_redirect = resolve_url(post_reset_redirect)
|
||||
try:
|
||||
uid_int = base36_to_int(uidb36)
|
||||
user = UserModel._default_manager.get(pk=uid_int)
|
||||
|
@ -242,7 +249,9 @@ def password_change(request,
|
|||
password_change_form=PasswordChangeForm,
|
||||
current_app=None, extra_context=None):
|
||||
if post_change_redirect is None:
|
||||
post_change_redirect = reverse('django.contrib.auth.views.password_change_done')
|
||||
post_change_redirect = reverse('password_change_done')
|
||||
else:
|
||||
post_change_redirect = resolve_url(post_change_redirect)
|
||||
if request.method == "POST":
|
||||
form = password_change_form(user=request.user, data=request.POST)
|
||||
if form.is_valid():
|
||||
|
|
|
@ -4,7 +4,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Django\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-05-02 16:18+0200\n"
|
||||
"POT-Creation-Date: 2013-05-25 14:19+0200\n"
|
||||
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||
"Last-Translator: Django team\n"
|
||||
"Language-Team: English <en@li.org>\n"
|
||||
|
@ -269,9 +269,11 @@ msgstr ""
|
|||
|
||||
#: templates/comments/preview.html:11
|
||||
msgid "Please correct the error below"
|
||||
msgid_plural "Please correct the errors below"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
msgstr ""
|
||||
|
||||
#: templates/comments/preview.html:11
|
||||
msgid "Please correct the errors below"
|
||||
msgstr ""
|
||||
|
||||
#: templates/comments/preview.html:16
|
||||
msgid "Post your comment"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<form action="{% comment_form_target %}" method="post">{% csrf_token %}
|
||||
{% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
|
||||
{% if form.errors %}
|
||||
<h1>{% blocktrans count counter=form.errors|length %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1>
|
||||
<h1>{% if form.errors|length == 1 %}{% trans "Please correct the error below" %}{% else %}{% trans "Please correct the errors below" %}{% endif %}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Preview your comment" %}</h1>
|
||||
<blockquote>{{ comment|linebreaks }}</blockquote>
|
||||
|
|
|
@ -35,9 +35,10 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
|||
fields.
|
||||
"""
|
||||
|
||||
def __init__(self, ct_field="content_type", fk_field="object_id"):
|
||||
def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True):
|
||||
self.ct_field = ct_field
|
||||
self.fk_field = fk_field
|
||||
self.for_concrete_model = for_concrete_model
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
self.name = name
|
||||
|
@ -63,7 +64,8 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
|||
|
||||
def get_content_type(self, obj=None, id=None, using=None):
|
||||
if obj is not None:
|
||||
return ContentType.objects.db_manager(obj._state.db).get_for_model(obj)
|
||||
return ContentType.objects.db_manager(obj._state.db).get_for_model(
|
||||
obj, for_concrete_model=self.for_concrete_model)
|
||||
elif id:
|
||||
return ContentType.objects.db_manager(using).get_for_id(id)
|
||||
else:
|
||||
|
@ -160,6 +162,8 @@ class GenericRelation(ForeignObject):
|
|||
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
|
||||
self.content_type_field_name = kwargs.pop("content_type_field", "content_type")
|
||||
|
||||
self.for_concrete_model = kwargs.pop("for_concrete_model", True)
|
||||
|
||||
kwargs['blank'] = True
|
||||
kwargs['editable'] = False
|
||||
kwargs['serialize'] = False
|
||||
|
@ -201,7 +205,7 @@ class GenericRelation(ForeignObject):
|
|||
# Save a reference to which model this class is on for future use
|
||||
self.model = cls
|
||||
# Add the descriptor for the relation
|
||||
setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self))
|
||||
setattr(cls, self.name, ReverseGenericRelatedObjectsDescriptor(self, self.for_concrete_model))
|
||||
|
||||
def contribute_to_related_class(self, cls, related):
|
||||
pass
|
||||
|
@ -216,7 +220,8 @@ class GenericRelation(ForeignObject):
|
|||
"""
|
||||
Returns the content type associated with this field's model.
|
||||
"""
|
||||
return ContentType.objects.get_for_model(self.model)
|
||||
return ContentType.objects.get_for_model(self.model,
|
||||
for_concrete_model=self.for_concrete_model)
|
||||
|
||||
def get_extra_restriction(self, where_class, alias, remote_alias):
|
||||
field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0]
|
||||
|
@ -232,7 +237,8 @@ class GenericRelation(ForeignObject):
|
|||
"""
|
||||
return self.rel.to._base_manager.db_manager(using).filter(**{
|
||||
"%s__pk" % self.content_type_field_name:
|
||||
ContentType.objects.db_manager(using).get_for_model(self.model).pk,
|
||||
ContentType.objects.db_manager(using).get_for_model(
|
||||
self.model, for_concrete_model=self.for_concrete_model).pk,
|
||||
"%s__in" % self.object_id_field_name:
|
||||
[obj.pk for obj in objs]
|
||||
})
|
||||
|
@ -247,8 +253,9 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
|||
"article.publications", the publications attribute is a
|
||||
ReverseGenericRelatedObjectsDescriptor instance.
|
||||
"""
|
||||
def __init__(self, field):
|
||||
def __init__(self, field, for_concrete_model=True):
|
||||
self.field = field
|
||||
self.for_concrete_model = for_concrete_model
|
||||
|
||||
def __get__(self, instance, instance_type=None):
|
||||
if instance is None:
|
||||
|
@ -261,7 +268,8 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
|||
RelatedManager = create_generic_related_manager(superclass)
|
||||
|
||||
qn = connection.ops.quote_name
|
||||
content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(instance)
|
||||
content_type = ContentType.objects.db_manager(instance._state.db).get_for_model(
|
||||
instance, for_concrete_model=self.for_concrete_model)
|
||||
|
||||
join_cols = self.field.get_joining_columns(reverse_join=True)[0]
|
||||
manager = RelatedManager(
|
||||
|
@ -376,7 +384,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||
"""
|
||||
|
||||
def __init__(self, data=None, files=None, instance=None, save_as_new=None,
|
||||
prefix=None, queryset=None):
|
||||
prefix=None, queryset=None, **kwargs):
|
||||
opts = self.model._meta
|
||||
self.instance = instance
|
||||
self.rel_name = '-'.join((
|
||||
|
@ -389,12 +397,14 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||
if queryset is None:
|
||||
queryset = self.model._default_manager
|
||||
qs = queryset.filter(**{
|
||||
self.ct_field.name: ContentType.objects.get_for_model(self.instance),
|
||||
self.ct_field.name: ContentType.objects.get_for_model(
|
||||
self.instance, for_concrete_model=self.for_concrete_model),
|
||||
self.ct_fk_field.name: self.instance.pk,
|
||||
})
|
||||
super(BaseGenericInlineFormSet, self).__init__(
|
||||
queryset=qs, data=data, files=files,
|
||||
prefix=prefix
|
||||
prefix=prefix,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -406,7 +416,8 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||
|
||||
def save_new(self, form, commit=True):
|
||||
kwargs = {
|
||||
self.ct_field.get_attname(): ContentType.objects.get_for_model(self.instance).pk,
|
||||
self.ct_field.get_attname(): ContentType.objects.get_for_model(
|
||||
self.instance, for_concrete_model=self.for_concrete_model).pk,
|
||||
self.ct_fk_field.get_attname(): self.instance.pk,
|
||||
}
|
||||
new_obj = self.model(**kwargs)
|
||||
|
@ -418,7 +429,8 @@ def generic_inlineformset_factory(model, form=ModelForm,
|
|||
fields=None, exclude=None,
|
||||
extra=3, can_order=False, can_delete=True,
|
||||
max_num=None,
|
||||
formfield_callback=None, validate_max=False):
|
||||
formfield_callback=None, validate_max=False,
|
||||
for_concrete_model=True):
|
||||
"""
|
||||
Returns a ``GenericInlineFormSet`` for the given kwargs.
|
||||
|
||||
|
@ -444,6 +456,7 @@ def generic_inlineformset_factory(model, form=ModelForm,
|
|||
validate_max=validate_max)
|
||||
FormSet.ct_field = ct_field
|
||||
FormSet.ct_fk_field = fk_field
|
||||
FormSet.for_concrete_model = for_concrete_model
|
||||
return FormSet
|
||||
|
||||
class GenericInlineModelAdmin(InlineModelAdmin):
|
||||
|
|
|
@ -118,11 +118,13 @@ class ContentTypeManager(models.Manager):
|
|||
|
||||
def _add_to_cache(self, using, ct):
|
||||
"""Insert a ContentType into the cache."""
|
||||
model = ct.model_class()
|
||||
key = (model._meta.app_label, model._meta.model_name)
|
||||
# Note it's possible for ContentType objects to be stale; model_class() will return None.
|
||||
# Hence, there is no reliance on model._meta.app_label here, just using the model fields instead.
|
||||
key = (ct.app_label, ct.model)
|
||||
self.__class__._cache.setdefault(using, {})[key] = ct
|
||||
self.__class__._cache.setdefault(using, {})[ct.id] = ct
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ContentType(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
@ -153,7 +155,6 @@ class ContentType(models.Model):
|
|||
|
||||
def model_class(self):
|
||||
"Returns the Python model class for this type of content."
|
||||
from django.db import models
|
||||
return models.get_model(self.app_label, self.model,
|
||||
only_installed=False)
|
||||
|
||||
|
|
|
@ -274,3 +274,10 @@ class ContentTypesTests(TestCase):
|
|||
model = 'OldModel',
|
||||
)
|
||||
self.assertEqual(six.text_type(ct), 'Old model')
|
||||
self.assertIsNone(ct.model_class())
|
||||
|
||||
# Make sure stale ContentTypes can be fetched like any other object.
|
||||
# Before Django 1.6 this caused a NoneType error in the caching mechanism.
|
||||
# Instead, just return the ContentType object and let the app detect stale states.
|
||||
ct_fetched = ContentType.objects.get_for_id(ct.pk)
|
||||
self.assertIsNone(ct_fetched.model_class())
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.flatpages.models import FlatPage
|
||||
from django.contrib.sites.models import get_current_site
|
||||
from django.core.xheaders import populate_xheaders
|
||||
from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import loader, RequestContext
|
||||
|
@ -70,5 +69,4 @@ def render_flatpage(request, f):
|
|||
'flatpage': f,
|
||||
})
|
||||
response = HttpResponse(t.render(c))
|
||||
populate_xheaders(request, response, FlatPage, f.id)
|
||||
return response
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
|
||||
class WizardViewCookieModified(SuspiciousOperation):
|
||||
"""Signature of cookie modified"""
|
||||
pass
|
|
@ -1,8 +1,8 @@
|
|||
import json
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.core.signing import BadSignature
|
||||
|
||||
from django.contrib.formtools.exceptions import WizardViewCookieModified
|
||||
from django.contrib.formtools.wizard import storage
|
||||
|
||||
|
||||
|
@ -21,7 +21,7 @@ class CookieStorage(storage.BaseStorage):
|
|||
except KeyError:
|
||||
data = None
|
||||
except BadSignature:
|
||||
raise SuspiciousOperation('WizardView cookie manipulated')
|
||||
raise WizardViewCookieModified('WizardView cookie manipulated')
|
||||
if data is None:
|
||||
return None
|
||||
return json.loads(data, cls=json.JSONDecoder)
|
||||
|
|
|
@ -11,6 +11,10 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
from django.db.backends.postgresql_psycopg2.base import DatabaseOperations
|
||||
from django.db.utils import DatabaseError
|
||||
from django.utils import six
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from .models import GeometryColumns, SpatialRefSys
|
||||
|
||||
|
||||
#### Classes used in constructing PostGIS spatial SQL ####
|
||||
class PostGISOperator(SpatialOperation):
|
||||
|
@ -62,6 +66,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
|
||||
name = 'postgis'
|
||||
postgis = True
|
||||
geom_func_prefix = 'ST_'
|
||||
version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
|
||||
valid_aggregates = dict([(k, None) for k in
|
||||
('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
|
||||
|
@ -72,45 +77,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
def __init__(self, connection):
|
||||
super(PostGISOperations, self).__init__(connection)
|
||||
|
||||
# Trying to get the PostGIS version because the function
|
||||
# signatures will depend on the version used. The cost
|
||||
# here is a database query to determine the version, which
|
||||
# can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
|
||||
# comprising user-supplied values for the major, minor, and
|
||||
# subminor revision of PostGIS.
|
||||
try:
|
||||
if hasattr(settings, 'POSTGIS_VERSION'):
|
||||
vtup = settings.POSTGIS_VERSION
|
||||
if len(vtup) == 3:
|
||||
# The user-supplied PostGIS version.
|
||||
version = vtup
|
||||
else:
|
||||
# This was the old documented way, but it's stupid to
|
||||
# include the string.
|
||||
version = vtup[1:4]
|
||||
else:
|
||||
vtup = self.postgis_version_tuple()
|
||||
version = vtup[1:]
|
||||
|
||||
# Getting the prefix -- even though we don't officially support
|
||||
# PostGIS 1.2 anymore, keeping it anyway in case a prefix change
|
||||
# for something else is necessary.
|
||||
if version >= (1, 2, 2):
|
||||
prefix = 'ST_'
|
||||
else:
|
||||
prefix = ''
|
||||
|
||||
self.geom_func_prefix = prefix
|
||||
self.spatial_version = version
|
||||
except DatabaseError:
|
||||
raise ImproperlyConfigured(
|
||||
'Cannot determine PostGIS version for database "%s". '
|
||||
'GeoDjango requires at least PostGIS version 1.3. '
|
||||
'Was the database created from a spatial database '
|
||||
'template?' % self.connection.settings_dict['NAME']
|
||||
)
|
||||
# TODO: Raise helpful exceptions as they become known.
|
||||
|
||||
prefix = self.geom_func_prefix
|
||||
# PostGIS-specific operators. The commented descriptions of these
|
||||
# operators come from Section 7.6 of the PostGIS 1.4 documentation.
|
||||
self.geometry_operators = {
|
||||
|
@ -188,13 +155,13 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
self.geometry_functions.update(self.distance_functions)
|
||||
|
||||
# Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
|
||||
if version < (1, 3, 4):
|
||||
if self.spatial_version < (1, 3, 4):
|
||||
GEOJSON = False
|
||||
else:
|
||||
GEOJSON = prefix + 'AsGeoJson'
|
||||
|
||||
# ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
|
||||
if version >= (1, 4, 0):
|
||||
if self.spatial_version >= (1, 4, 0):
|
||||
GEOHASH = 'ST_GeoHash'
|
||||
BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
|
||||
self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
|
||||
|
@ -202,7 +169,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
GEOHASH, BOUNDINGCIRCLE = False, False
|
||||
|
||||
# Geography type support added in 1.5.
|
||||
if version >= (1, 5, 0):
|
||||
if self.spatial_version >= (1, 5, 0):
|
||||
self.geography = True
|
||||
# Only a subset of the operators and functions are available
|
||||
# for the geography type.
|
||||
|
@ -217,7 +184,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
}
|
||||
|
||||
# Native geometry type support added in PostGIS 2.0.
|
||||
if version >= (2, 0, 0):
|
||||
if self.spatial_version >= (2, 0, 0):
|
||||
self.geometry = True
|
||||
|
||||
# Creating a dictionary lookup of all GIS terms for PostGIS.
|
||||
|
@ -260,7 +227,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
self.union = prefix + 'Union'
|
||||
self.unionagg = prefix + 'Union'
|
||||
|
||||
if version >= (2, 0, 0):
|
||||
if self.spatial_version >= (2, 0, 0):
|
||||
self.extent3d = prefix + '3DExtent'
|
||||
self.length3d = prefix + '3DLength'
|
||||
self.perimeter3d = prefix + '3DPerimeter'
|
||||
|
@ -269,6 +236,30 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
self.length3d = prefix + 'Length3D'
|
||||
self.perimeter3d = prefix + 'Perimeter3D'
|
||||
|
||||
@cached_property
|
||||
def spatial_version(self):
|
||||
"""Determine the version of the PostGIS library."""
|
||||
# Trying to get the PostGIS version because the function
|
||||
# signatures will depend on the version used. The cost
|
||||
# here is a database query to determine the version, which
|
||||
# can be mitigated by setting `POSTGIS_VERSION` with a 3-tuple
|
||||
# comprising user-supplied values for the major, minor, and
|
||||
# subminor revision of PostGIS.
|
||||
if hasattr(settings, 'POSTGIS_VERSION'):
|
||||
version = settings.POSTGIS_VERSION
|
||||
else:
|
||||
try:
|
||||
vtup = self.postgis_version_tuple()
|
||||
except DatabaseError:
|
||||
raise ImproperlyConfigured(
|
||||
'Cannot determine PostGIS version for database "%s". '
|
||||
'GeoDjango requires at least PostGIS version 1.3. '
|
||||
'Was the database created from a spatial database '
|
||||
'template?' % self.connection.settings_dict['NAME']
|
||||
)
|
||||
version = vtup[1:]
|
||||
return version
|
||||
|
||||
def check_aggregate_support(self, aggregate):
|
||||
"""
|
||||
Checks if the given aggregate name is supported (that is, if it's
|
||||
|
@ -572,9 +563,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||
|
||||
# Routines for getting the OGC-compliant models.
|
||||
def geometry_columns(self):
|
||||
from django.contrib.gis.db.backends.postgis.models import GeometryColumns
|
||||
return GeometryColumns
|
||||
|
||||
def spatial_ref_sys(self):
|
||||
from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
|
||||
return SpatialRefSys
|
||||
|
|
|
@ -384,7 +384,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||
@property
|
||||
def wkt(self):
|
||||
"Returns the WKT (Well-Known Text) representation of this Geometry."
|
||||
return wkt_w(self.hasz and 3 or 2).write(self).decode()
|
||||
return wkt_w(3 if self.hasz else 2).write(self).decode()
|
||||
|
||||
@property
|
||||
def hex(self):
|
||||
|
@ -395,7 +395,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||
"""
|
||||
# A possible faster, all-python, implementation:
|
||||
# str(self.wkb).encode('hex')
|
||||
return wkb_w(self.hasz and 3 or 2).write_hex(self)
|
||||
return wkb_w(3 if self.hasz else 2).write_hex(self)
|
||||
|
||||
@property
|
||||
def hexewkb(self):
|
||||
|
@ -407,7 +407,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||
if self.hasz and not GEOS_PREPARE:
|
||||
# See: http://trac.osgeo.org/geos/ticket/216
|
||||
raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.')
|
||||
return ewkb_w(self.hasz and 3 or 2).write_hex(self)
|
||||
return ewkb_w(3 if self.hasz else 2).write_hex(self)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
|
@ -427,7 +427,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||
as a Python buffer. SRID and Z values are not included, use the
|
||||
`ewkb` property instead.
|
||||
"""
|
||||
return wkb_w(self.hasz and 3 or 2).write(self)
|
||||
return wkb_w(3 if self.hasz else 2).write(self)
|
||||
|
||||
@property
|
||||
def ewkb(self):
|
||||
|
@ -439,7 +439,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||
if self.hasz and not GEOS_PREPARE:
|
||||
# See: http://trac.osgeo.org/geos/ticket/216
|
||||
raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.')
|
||||
return ewkb_w(self.hasz and 3 or 2).write(self)
|
||||
return ewkb_w(3 if self.hasz else 2).write(self)
|
||||
|
||||
@property
|
||||
def kml(self):
|
||||
|
|
|
@ -546,7 +546,7 @@ class LayerMapping(object):
|
|||
# Attempting to save.
|
||||
m.save(using=self.using)
|
||||
num_saved += 1
|
||||
if verbose: stream.write('%s: %s\n' % (is_update and 'Updated' or 'Saved', m))
|
||||
if verbose: stream.write('%s: %s\n' % ('Updated' if is_update else 'Saved', m))
|
||||
except SystemExit:
|
||||
raise
|
||||
except Exception as msg:
|
||||
|
|
|
@ -4,7 +4,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: Django\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-05-02 16:18+0200\n"
|
||||
"POT-Creation-Date: 2013-05-18 23:10+0200\n"
|
||||
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||
"Last-Translator: Django team\n"
|
||||
"Language-Team: English <en@li.org>\n"
|
||||
|
@ -237,54 +237,60 @@ msgctxt "naturaltime"
|
|||
msgid "%(delta)s ago"
|
||||
msgstr ""
|
||||
|
||||
#: templatetags/humanize.py:194 templatetags/humanize.py:216
|
||||
#: templatetags/humanize.py:194 templatetags/humanize.py:219
|
||||
msgid "now"
|
||||
msgstr ""
|
||||
|
||||
#: templatetags/humanize.py:197
|
||||
#. Translators: \\u00a0 is non-breaking space
|
||||
#: templatetags/humanize.py:198
|
||||
#, python-format
|
||||
msgid "a second ago"
|
||||
msgid_plural "%(count)s seconds ago"
|
||||
msgid_plural "%(count)s\\u00a0seconds ago"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: templatetags/humanize.py:202
|
||||
#. Translators: \\u00a0 is non-breaking space
|
||||
#: templatetags/humanize.py:204
|
||||
#, python-format
|
||||
msgid "a minute ago"
|
||||
msgid_plural "%(count)s minutes ago"
|
||||
msgid_plural "%(count)s\\u00a0minutes ago"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: templatetags/humanize.py:207
|
||||
#. Translators: \\u00a0 is non-breaking space
|
||||
#: templatetags/humanize.py:210
|
||||
#, python-format
|
||||
msgid "an hour ago"
|
||||
msgid_plural "%(count)s hours ago"
|
||||
msgid_plural "%(count)s\\u00a0hours ago"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: templatetags/humanize.py:213
|
||||
#: templatetags/humanize.py:216
|
||||
#, python-format
|
||||
msgctxt "naturaltime"
|
||||
msgid "%(delta)s from now"
|
||||
msgstr ""
|
||||
|
||||
#: templatetags/humanize.py:219
|
||||
#. Translators: \\u00a0 is non-breaking space
|
||||
#: templatetags/humanize.py:223
|
||||
#, python-format
|
||||
msgid "a second from now"
|
||||
msgid_plural "%(count)s seconds from now"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: templatetags/humanize.py:224
|
||||
#, python-format
|
||||
msgid "a minute from now"
|
||||
msgid_plural "%(count)s minutes from now"
|
||||
msgid_plural "%(count)s\\u00a0seconds from now"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#. Translators: \\u00a0 is non-breaking space
|
||||
#: templatetags/humanize.py:229
|
||||
#, python-format
|
||||
msgid "an hour from now"
|
||||
msgid_plural "%(count)s hours from now"
|
||||
msgid "a minute from now"
|
||||
msgid_plural "%(count)s\\u00a0minutes from now"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#. Translators: \\u00a0 is non-breaking space
|
||||
#: templatetags/humanize.py:235
|
||||
#, python-format
|
||||
msgid "an hour from now"
|
||||
msgid_plural "%(count)s\\u00a0hours from now"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
|
|
@ -194,17 +194,20 @@ def naturaltime(value):
|
|||
return _('now')
|
||||
elif delta.seconds < 60:
|
||||
return ungettext(
|
||||
'a second ago', '%(count)s seconds ago', delta.seconds
|
||||
# Translators: \\u00a0 is non-breaking space
|
||||
'a second ago', '%(count)s\u00a0seconds ago', delta.seconds
|
||||
) % {'count': delta.seconds}
|
||||
elif delta.seconds // 60 < 60:
|
||||
count = delta.seconds // 60
|
||||
return ungettext(
|
||||
'a minute ago', '%(count)s minutes ago', count
|
||||
# Translators: \\u00a0 is non-breaking space
|
||||
'a minute ago', '%(count)s\u00a0minutes ago', count
|
||||
) % {'count': count}
|
||||
else:
|
||||
count = delta.seconds // 60 // 60
|
||||
return ungettext(
|
||||
'an hour ago', '%(count)s hours ago', count
|
||||
# Translators: \\u00a0 is non-breaking space
|
||||
'an hour ago', '%(count)s\u00a0hours ago', count
|
||||
) % {'count': count}
|
||||
else:
|
||||
delta = value - now
|
||||
|
@ -216,15 +219,18 @@ def naturaltime(value):
|
|||
return _('now')
|
||||
elif delta.seconds < 60:
|
||||
return ungettext(
|
||||
'a second from now', '%(count)s seconds from now', delta.seconds
|
||||
# Translators: \\u00a0 is non-breaking space
|
||||
'a second from now', '%(count)s\u00a0seconds from now', delta.seconds
|
||||
) % {'count': delta.seconds}
|
||||
elif delta.seconds // 60 < 60:
|
||||
count = delta.seconds // 60
|
||||
return ungettext(
|
||||
'a minute from now', '%(count)s minutes from now', count
|
||||
# Translators: \\u00a0 is non-breaking space
|
||||
'a minute from now', '%(count)s\u00a0minutes from now', count
|
||||
) % {'count': count}
|
||||
else:
|
||||
count = delta.seconds // 60 // 60
|
||||
return ungettext(
|
||||
'an hour from now', '%(count)s hours from now', count
|
||||
# Translators: \\u00a0 is non-breaking space
|
||||
'an hour from now', '%(count)s\u00a0hours from now', count
|
||||
) % {'count': count}
|
||||
|
|
|
@ -19,6 +19,8 @@ from django.utils.translation import ugettext as _
|
|||
from django.utils import tzinfo
|
||||
from django.utils.unittest import skipIf
|
||||
|
||||
from i18n import TransRealMixin
|
||||
|
||||
|
||||
# Mock out datetime in some tests so they don't fail occasionally when they
|
||||
# run too slow. Use a fixed datetime for datetime.now(). DST change in
|
||||
|
@ -36,7 +38,7 @@ class MockDateTime(datetime.datetime):
|
|||
return now.replace(tzinfo=tz) + tz.utcoffset(now)
|
||||
|
||||
|
||||
class HumanizeTests(TestCase):
|
||||
class HumanizeTests(TransRealMixin, TestCase):
|
||||
|
||||
def humanize_tester(self, test_list, result_list, method):
|
||||
for test_content, result in zip(test_list, result_list):
|
||||
|
@ -195,22 +197,22 @@ class HumanizeTests(TestCase):
|
|||
result_list = [
|
||||
'now',
|
||||
'a second ago',
|
||||
'30 seconds ago',
|
||||
'30\xa0seconds ago',
|
||||
'a minute ago',
|
||||
'2 minutes ago',
|
||||
'2\xa0minutes ago',
|
||||
'an hour ago',
|
||||
'23 hours ago',
|
||||
'1 day ago',
|
||||
'1 year, 4 months ago',
|
||||
'23\xa0hours ago',
|
||||
'1\xa0day ago',
|
||||
'1\xa0year, 4\xa0months ago',
|
||||
'a second from now',
|
||||
'30 seconds from now',
|
||||
'30\xa0seconds from now',
|
||||
'a minute from now',
|
||||
'2 minutes from now',
|
||||
'2\xa0minutes from now',
|
||||
'an hour from now',
|
||||
'23 hours from now',
|
||||
'1 day from now',
|
||||
'2 days, 6 hours from now',
|
||||
'1 year, 4 months from now',
|
||||
'23\xa0hours from now',
|
||||
'1\xa0day from now',
|
||||
'2\xa0days, 6\xa0hours from now',
|
||||
'1\xa0year, 4\xa0months from now',
|
||||
'now',
|
||||
'now',
|
||||
]
|
||||
|
@ -218,8 +220,8 @@ class HumanizeTests(TestCase):
|
|||
# date in naive arithmetic is only 2 days and 5 hours after in
|
||||
# aware arithmetic.
|
||||
result_list_with_tz_support = result_list[:]
|
||||
assert result_list_with_tz_support[-4] == '2 days, 6 hours from now'
|
||||
result_list_with_tz_support[-4] == '2 days, 5 hours from now'
|
||||
assert result_list_with_tz_support[-4] == '2\xa0days, 6\xa0hours from now'
|
||||
result_list_with_tz_support[-4] == '2\xa0days, 5\xa0hours from now'
|
||||
|
||||
orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime
|
||||
try:
|
||||
|
|
|
@ -2,6 +2,8 @@ from __future__ import unicode_literals
|
|||
|
||||
import base64
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
try:
|
||||
from django.utils.six.moves import cPickle as pickle
|
||||
except ImportError:
|
||||
|
@ -14,7 +16,9 @@ from django.utils.crypto import constant_time_compare
|
|||
from django.utils.crypto import get_random_string
|
||||
from django.utils.crypto import salted_hmac
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_bytes
|
||||
from django.utils.encoding import force_bytes, force_text
|
||||
|
||||
from django.contrib.sessions.exceptions import SuspiciousSession
|
||||
|
||||
# session_key should not be case sensitive because some backends can store it
|
||||
# on case insensitive file systems.
|
||||
|
@ -94,12 +98,16 @@ class SessionBase(object):
|
|||
hash, pickled = encoded_data.split(b':', 1)
|
||||
expected_hash = self._hash(pickled)
|
||||
if not constant_time_compare(hash.decode(), expected_hash):
|
||||
raise SuspiciousOperation("Session data corrupted")
|
||||
raise SuspiciousSession("Session data corrupted")
|
||||
else:
|
||||
return pickle.loads(pickled)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# ValueError, SuspiciousOperation, unpickling exceptions. If any of
|
||||
# these happen, just return an empty dictionary (an empty session).
|
||||
if isinstance(e, SuspiciousOperation):
|
||||
logger = logging.getLogger('django.security.%s' %
|
||||
e.__class__.__name__)
|
||||
logger.warning(force_text(e))
|
||||
return {}
|
||||
|
||||
def update(self, dict_):
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
Cached, database-backed sessions.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.sessions.backends.db import SessionStore as DBStore
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
KEY_PREFIX = "django.contrib.sessions.cached_db"
|
||||
|
||||
|
@ -41,7 +44,11 @@ class SessionStore(DBStore):
|
|||
data = self.decode(s.session_data)
|
||||
cache.set(self.cache_key, data,
|
||||
self.get_expiry_age(expiry=s.expire_date))
|
||||
except (Session.DoesNotExist, SuspiciousOperation):
|
||||
except (Session.DoesNotExist, SuspiciousOperation) as e:
|
||||
if isinstance(e, SuspiciousOperation):
|
||||
logger = logging.getLogger('django.security.%s' %
|
||||
e.__class__.__name__)
|
||||
logger.warning(force_text(e))
|
||||
self.create()
|
||||
data = {}
|
||||
return data
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import logging
|
||||
|
||||
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db import IntegrityError, transaction, router
|
||||
from django.utils import timezone
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
class SessionStore(SessionBase):
|
||||
"""
|
||||
|
@ -18,7 +20,11 @@ class SessionStore(SessionBase):
|
|||
expire_date__gt=timezone.now()
|
||||
)
|
||||
return self.decode(s.session_data)
|
||||
except (Session.DoesNotExist, SuspiciousOperation):
|
||||
except (Session.DoesNotExist, SuspiciousOperation) as e:
|
||||
if isinstance(e, SuspiciousOperation):
|
||||
logger = logging.getLogger('django.security.%s' %
|
||||
e.__class__.__name__)
|
||||
logger.warning(force_text(e))
|
||||
self.create()
|
||||
return {}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import datetime
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
@ -8,6 +9,9 @@ from django.conf import settings
|
|||
from django.contrib.sessions.backends.base import SessionBase, CreateError, VALID_KEY_CHARS
|
||||
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from django.contrib.sessions.exceptions import InvalidSessionKey
|
||||
|
||||
class SessionStore(SessionBase):
|
||||
"""
|
||||
|
@ -48,7 +52,7 @@ class SessionStore(SessionBase):
|
|||
# should always be md5s, so they should never contain directory
|
||||
# components.
|
||||
if not set(session_key).issubset(set(VALID_KEY_CHARS)):
|
||||
raise SuspiciousOperation(
|
||||
raise InvalidSessionKey(
|
||||
"Invalid characters in session key")
|
||||
|
||||
return os.path.join(self.storage_path, self.file_prefix + session_key)
|
||||
|
@ -75,7 +79,11 @@ class SessionStore(SessionBase):
|
|||
if file_data:
|
||||
try:
|
||||
session_data = self.decode(file_data)
|
||||
except (EOFError, SuspiciousOperation):
|
||||
except (EOFError, SuspiciousOperation) as e:
|
||||
if isinstance(e, SuspiciousOperation):
|
||||
logger = logging.getLogger('django.security.%s' %
|
||||
e.__class__.__name__)
|
||||
logger.warning(force_text(e))
|
||||
self.create()
|
||||
|
||||
# Remove expired sessions.
|
||||
|
@ -86,7 +94,7 @@ class SessionStore(SessionBase):
|
|||
session_data = {}
|
||||
self.delete()
|
||||
self.create()
|
||||
except IOError:
|
||||
except (IOError, SuspiciousOperation):
|
||||
self.create()
|
||||
return session_data
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
|
||||
class InvalidSessionKey(SuspiciousOperation):
|
||||
"""Invalid characters in session key"""
|
||||
pass
|
||||
|
||||
|
||||
class SuspiciousSession(SuspiciousOperation):
|
||||
"""The session may be tampered with"""
|
||||
pass
|
|
@ -1,3 +1,4 @@
|
|||
import base64
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import shutil
|
||||
|
@ -15,14 +16,16 @@ from django.contrib.sessions.models import Session
|
|||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.cache import get_cache
|
||||
from django.core import management
|
||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponse
|
||||
from django.test import TestCase, RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
from django.test.utils import override_settings, patch_logger
|
||||
from django.utils import six
|
||||
from django.utils import timezone
|
||||
from django.utils import unittest
|
||||
|
||||
from django.contrib.sessions.exceptions import InvalidSessionKey
|
||||
|
||||
|
||||
class SessionTestsMixin(object):
|
||||
# This does not inherit from TestCase to avoid any tests being run with this
|
||||
|
@ -272,6 +275,15 @@ class SessionTestsMixin(object):
|
|||
encoded = self.session.encode(data)
|
||||
self.assertEqual(self.session.decode(encoded), data)
|
||||
|
||||
def test_decode_failure_logged_to_security(self):
|
||||
bad_encode = base64.b64encode(b'flaskdj:alkdjf')
|
||||
with patch_logger('django.security.SuspiciousSession', 'warning') as calls:
|
||||
self.assertEqual({}, self.session.decode(bad_encode))
|
||||
# check that the failed decode is logged
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertTrue('corrupted' in calls[0])
|
||||
|
||||
|
||||
def test_actual_expiry(self):
|
||||
# Regression test for #19200
|
||||
old_session_key = None
|
||||
|
@ -403,14 +415,21 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
|
|||
self.assertRaises(ImproperlyConfigured, self.backend)
|
||||
|
||||
def test_invalid_key_backslash(self):
|
||||
# Ensure we don't allow directory-traversal
|
||||
self.assertRaises(SuspiciousOperation,
|
||||
self.backend("a\\b\\c").load)
|
||||
# This key should be refused and a new session should be created
|
||||
self.assertTrue(self.backend("a\\b\\c").load())
|
||||
|
||||
def test_invalid_key_backslash(self):
|
||||
# Ensure we don't allow directory-traversal.
|
||||
# This is tested directly on _key_to_file, as load() will swallow
|
||||
# a SuspiciousOperation in the same way as an IOError - by creating
|
||||
# a new session, making it unclear whether the slashes were detected.
|
||||
self.assertRaises(InvalidSessionKey,
|
||||
self.backend()._key_to_file, "a\\b\\c")
|
||||
|
||||
def test_invalid_key_forwardslash(self):
|
||||
# Ensure we don't allow directory-traversal
|
||||
self.assertRaises(SuspiciousOperation,
|
||||
self.backend("a/b/c").load)
|
||||
self.assertRaises(InvalidSessionKey,
|
||||
self.backend()._key_to_file, "a/b/c")
|
||||
|
||||
@override_settings(SESSION_ENGINE="django.contrib.sessions.backends.file")
|
||||
def test_clearsessions_command(self):
|
||||
|
|
|
@ -94,7 +94,7 @@ class Sitemap(object):
|
|||
'location': loc,
|
||||
'lastmod': self.__get('lastmod', item, None),
|
||||
'changefreq': self.__get('changefreq', item, None),
|
||||
'priority': str(priority is not None and priority or ''),
|
||||
'priority': str(priority if priority is not None else ''),
|
||||
}
|
||||
urls.append(url_info)
|
||||
return urls
|
||||
|
|
|
@ -245,7 +245,7 @@ def find(path, all=False):
|
|||
if matches:
|
||||
return matches
|
||||
# No match.
|
||||
return all and [] or None
|
||||
return [] if all else None
|
||||
|
||||
|
||||
def get_finders():
|
||||
|
|
|
@ -175,11 +175,9 @@ Type 'yes' to continue, or 'no' to cancel: """
|
|||
summary = template % {
|
||||
'modified_count': modified_count,
|
||||
'identifier': 'static file' + ('' if modified_count == 1 else 's'),
|
||||
'action': self.symlink and 'symlinked' or 'copied',
|
||||
'destination': (destination_path and " to '%s'"
|
||||
% destination_path or ''),
|
||||
'unmodified': (collected['unmodified'] and ', %s unmodified'
|
||||
% unmodified_count or ''),
|
||||
'action': 'symlinked' if self.symlink else 'copied',
|
||||
'destination': (" to '%s'" % destination_path if destination_path else ''),
|
||||
'unmodified': (', %s unmodified' % unmodified_count if collected['unmodified'] else ''),
|
||||
'post_processed': (collected['post_processed'] and
|
||||
', %s post-processed'
|
||||
% post_processed_count or ''),
|
||||
|
|
|
@ -15,6 +15,10 @@ class CacheKeyWarning(DjangoRuntimeWarning):
|
|||
pass
|
||||
|
||||
|
||||
# Stub class to ensure not passing in a `timeout` argument results in
|
||||
# the default timeout
|
||||
DEFAULT_TIMEOUT = object()
|
||||
|
||||
# Memcached does not accept keys longer than this.
|
||||
MEMCACHE_MAX_KEY_LENGTH = 250
|
||||
|
||||
|
@ -84,7 +88,7 @@ class BaseCache(object):
|
|||
new_key = self.key_func(key, self.key_prefix, version)
|
||||
return new_key
|
||||
|
||||
def add(self, key, value, timeout=None, version=None):
|
||||
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
"""
|
||||
Set a value in the cache if the key does not already exist. If
|
||||
timeout is given, that timeout will be used for the key; otherwise
|
||||
|
@ -101,7 +105,7 @@ class BaseCache(object):
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set(self, key, value, timeout=None, version=None):
|
||||
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
"""
|
||||
Set a value in the cache. If timeout is given, that timeout will be
|
||||
used for the key; otherwise the default cache timeout will be used.
|
||||
|
@ -163,7 +167,7 @@ class BaseCache(object):
|
|||
# if a subclass overrides it.
|
||||
return self.has_key(key)
|
||||
|
||||
def set_many(self, data, timeout=None, version=None):
|
||||
def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
"""
|
||||
Set a bunch of values in the cache at once from a dict of key/value
|
||||
pairs. For certain backends (memcached), this is much more efficient
|
||||
|
|
|
@ -9,7 +9,7 @@ except ImportError:
|
|||
import pickle
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache.backends.base import BaseCache
|
||||
from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
|
||||
from django.db import connections, transaction, router, DatabaseError
|
||||
from django.utils import timezone, six
|
||||
from django.utils.encoding import force_bytes
|
||||
|
@ -65,6 +65,7 @@ class DatabaseCache(BaseDatabaseCache):
|
|||
if row is None:
|
||||
return default
|
||||
now = timezone.now()
|
||||
|
||||
if row[2] < now:
|
||||
db = router.db_for_write(self.cache_model_class)
|
||||
cursor = connections[db].cursor()
|
||||
|
@ -74,18 +75,18 @@ class DatabaseCache(BaseDatabaseCache):
|
|||
value = connections[db].ops.process_clob(row[1])
|
||||
return pickle.loads(base64.b64decode(force_bytes(value)))
|
||||
|
||||
def set(self, key, value, timeout=None, version=None):
|
||||
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
self._base_set('set', key, value, timeout)
|
||||
|
||||
def add(self, key, value, timeout=None, version=None):
|
||||
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
return self._base_set('add', key, value, timeout)
|
||||
|
||||
def _base_set(self, mode, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT):
|
||||
if timeout == DEFAULT_TIMEOUT:
|
||||
timeout = self.default_timeout
|
||||
db = router.db_for_write(self.cache_model_class)
|
||||
table = connections[db].ops.quote_name(self._table)
|
||||
|
@ -95,7 +96,9 @@ class DatabaseCache(BaseDatabaseCache):
|
|||
num = cursor.fetchone()[0]
|
||||
now = timezone.now()
|
||||
now = now.replace(microsecond=0)
|
||||
if settings.USE_TZ:
|
||||
if timeout is None:
|
||||
exp = datetime.max
|
||||
elif settings.USE_TZ:
|
||||
exp = datetime.utcfromtimestamp(time.time() + timeout)
|
||||
else:
|
||||
exp = datetime.fromtimestamp(time.time() + timeout)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
"Dummy cache backend"
|
||||
|
||||
from django.core.cache.backends.base import BaseCache
|
||||
from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
|
||||
|
||||
class DummyCache(BaseCache):
|
||||
def __init__(self, host, *args, **kwargs):
|
||||
BaseCache.__init__(self, *args, **kwargs)
|
||||
|
||||
def add(self, key, value, timeout=None, version=None):
|
||||
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
return True
|
||||
|
@ -16,7 +16,7 @@ class DummyCache(BaseCache):
|
|||
self.validate_key(key)
|
||||
return default
|
||||
|
||||
def set(self, key, value, timeout=None, version=None):
|
||||
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
|
||||
|
@ -32,7 +32,7 @@ class DummyCache(BaseCache):
|
|||
self.validate_key(key)
|
||||
return False
|
||||
|
||||
def set_many(self, data, timeout=0, version=None):
|
||||
def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
pass
|
||||
|
||||
def delete_many(self, keys, version=None):
|
||||
|
|
|
@ -9,9 +9,10 @@ try:
|
|||
except ImportError:
|
||||
import pickle
|
||||
|
||||
from django.core.cache.backends.base import BaseCache
|
||||
from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
|
||||
class FileBasedCache(BaseCache):
|
||||
def __init__(self, dir, params):
|
||||
BaseCache.__init__(self, params)
|
||||
|
@ -19,7 +20,7 @@ class FileBasedCache(BaseCache):
|
|||
if not os.path.exists(self._dir):
|
||||
self._createdir()
|
||||
|
||||
def add(self, key, value, timeout=None, version=None):
|
||||
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
if self.has_key(key, version=version):
|
||||
return False
|
||||
|
||||
|
@ -35,7 +36,7 @@ class FileBasedCache(BaseCache):
|
|||
with open(fname, 'rb') as f:
|
||||
exp = pickle.load(f)
|
||||
now = time.time()
|
||||
if exp < now:
|
||||
if exp is not None and exp < now:
|
||||
self._delete(fname)
|
||||
else:
|
||||
return pickle.load(f)
|
||||
|
@ -43,14 +44,14 @@ class FileBasedCache(BaseCache):
|
|||
pass
|
||||
return default
|
||||
|
||||
def set(self, key, value, timeout=None, version=None):
|
||||
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
|
||||
fname = self._key_to_file(key)
|
||||
dirname = os.path.dirname(fname)
|
||||
|
||||
if timeout is None:
|
||||
if timeout == DEFAULT_TIMEOUT:
|
||||
timeout = self.default_timeout
|
||||
|
||||
self._cull()
|
||||
|
@ -60,8 +61,8 @@ class FileBasedCache(BaseCache):
|
|||
os.makedirs(dirname)
|
||||
|
||||
with open(fname, 'wb') as f:
|
||||
now = time.time()
|
||||
pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL)
|
||||
expiry = None if timeout is None else time.time() + timeout
|
||||
pickle.dump(expiry, f, pickle.HIGHEST_PROTOCOL)
|
||||
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
|
|
@ -6,7 +6,7 @@ try:
|
|||
except ImportError:
|
||||
import pickle
|
||||
|
||||
from django.core.cache.backends.base import BaseCache
|
||||
from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
|
||||
from django.utils.synch import RWLock
|
||||
|
||||
# Global in-memory store of cache data. Keyed by name, to provide
|
||||
|
@ -23,7 +23,7 @@ class LocMemCache(BaseCache):
|
|||
self._expire_info = _expire_info.setdefault(name, {})
|
||||
self._lock = _locks.setdefault(name, RWLock())
|
||||
|
||||
def add(self, key, value, timeout=None, version=None):
|
||||
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
with self._lock.writer():
|
||||
|
@ -41,10 +41,8 @@ class LocMemCache(BaseCache):
|
|||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
with self._lock.reader():
|
||||
exp = self._expire_info.get(key)
|
||||
if exp is None:
|
||||
return default
|
||||
elif exp > time.time():
|
||||
exp = self._expire_info.get(key, 0)
|
||||
if exp is None or exp > time.time():
|
||||
try:
|
||||
pickled = self._cache[key]
|
||||
return pickle.loads(pickled)
|
||||
|
@ -58,15 +56,16 @@ class LocMemCache(BaseCache):
|
|||
pass
|
||||
return default
|
||||
|
||||
def _set(self, key, value, timeout=None):
|
||||
def _set(self, key, value, timeout=DEFAULT_TIMEOUT):
|
||||
if len(self._cache) >= self._max_entries:
|
||||
self._cull()
|
||||
if timeout is None:
|
||||
if timeout == DEFAULT_TIMEOUT:
|
||||
timeout = self.default_timeout
|
||||
expiry = None if timeout is None else time.time() + timeout
|
||||
self._cache[key] = value
|
||||
self._expire_info[key] = time.time() + timeout
|
||||
self._expire_info[key] = expiry
|
||||
|
||||
def set(self, key, value, timeout=None, version=None):
|
||||
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self.validate_key(key)
|
||||
with self._lock.writer():
|
||||
|
|
|
@ -4,7 +4,7 @@ import time
|
|||
import pickle
|
||||
from threading import local
|
||||
|
||||
from django.core.cache.backends.base import BaseCache, InvalidCacheBackendError
|
||||
from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
|
||||
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_str
|
||||
|
@ -36,12 +36,22 @@ class BaseMemcachedCache(BaseCache):
|
|||
|
||||
return self._client
|
||||
|
||||
def _get_memcache_timeout(self, timeout):
|
||||
def _get_memcache_timeout(self, timeout=DEFAULT_TIMEOUT):
|
||||
"""
|
||||
Memcached deals with long (> 30 days) timeouts in a special
|
||||
way. Call this function to obtain a safe value for your timeout.
|
||||
"""
|
||||
timeout = timeout or self.default_timeout
|
||||
if timeout == DEFAULT_TIMEOUT:
|
||||
return self.default_timeout
|
||||
|
||||
if timeout is None:
|
||||
# Using 0 in memcache sets a non-expiring timeout.
|
||||
return 0
|
||||
elif int(timeout) == 0:
|
||||
# Other cache backends treat 0 as set-and-expire. To achieve this
|
||||
# in memcache backends, a negative timeout must be passed.
|
||||
timeout = -1
|
||||
|
||||
if timeout > 2592000: # 60*60*24*30, 30 days
|
||||
# See http://code.google.com/p/memcached/wiki/FAQ
|
||||
# "You can set expire times up to 30 days in the future. After that
|
||||
|
@ -56,7 +66,7 @@ class BaseMemcachedCache(BaseCache):
|
|||
# Python 2 memcache requires the key to be a byte string.
|
||||
return force_str(super(BaseMemcachedCache, self).make_key(key, version))
|
||||
|
||||
def add(self, key, value, timeout=0, version=None):
|
||||
def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
return self._cache.add(key, value, self._get_memcache_timeout(timeout))
|
||||
|
||||
|
@ -67,7 +77,7 @@ class BaseMemcachedCache(BaseCache):
|
|||
return default
|
||||
return val
|
||||
|
||||
def set(self, key, value, timeout=0, version=None):
|
||||
def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
key = self.make_key(key, version=version)
|
||||
self._cache.set(key, value, self._get_memcache_timeout(timeout))
|
||||
|
||||
|
@ -125,7 +135,7 @@ class BaseMemcachedCache(BaseCache):
|
|||
raise ValueError("Key '%s' not found" % key)
|
||||
return val
|
||||
|
||||
def set_many(self, data, timeout=0, version=None):
|
||||
def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
|
||||
safe_data = {}
|
||||
for key, value in data.items():
|
||||
key = self.make_key(key, version=version)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""
|
||||
Global Django exception and warning classes.
|
||||
"""
|
||||
import logging
|
||||
from functools import reduce
|
||||
|
||||
|
||||
|
@ -9,37 +10,56 @@ class DjangoRuntimeWarning(RuntimeWarning):
|
|||
|
||||
|
||||
class ObjectDoesNotExist(Exception):
|
||||
"The requested object does not exist"
|
||||
"""The requested object does not exist"""
|
||||
silent_variable_failure = True
|
||||
|
||||
|
||||
class MultipleObjectsReturned(Exception):
|
||||
"The query returned multiple objects when only one was expected."
|
||||
"""The query returned multiple objects when only one was expected."""
|
||||
pass
|
||||
|
||||
|
||||
class SuspiciousOperation(Exception):
|
||||
"The user did something suspicious"
|
||||
"""The user did something suspicious"""
|
||||
|
||||
|
||||
class SuspiciousMultipartForm(SuspiciousOperation):
|
||||
"""Suspect MIME request in multipart form data"""
|
||||
pass
|
||||
|
||||
|
||||
class SuspiciousFileOperation(SuspiciousOperation):
|
||||
"""A Suspicious filesystem operation was attempted"""
|
||||
pass
|
||||
|
||||
|
||||
class DisallowedHost(SuspiciousOperation):
|
||||
"""HTTP_HOST header contains invalid value"""
|
||||
pass
|
||||
|
||||
|
||||
class DisallowedRedirect(SuspiciousOperation):
|
||||
"""Redirect to scheme not in allowed list"""
|
||||
pass
|
||||
|
||||
|
||||
class PermissionDenied(Exception):
|
||||
"The user did not have permission to do that"
|
||||
"""The user did not have permission to do that"""
|
||||
pass
|
||||
|
||||
|
||||
class ViewDoesNotExist(Exception):
|
||||
"The requested view does not exist"
|
||||
"""The requested view does not exist"""
|
||||
pass
|
||||
|
||||
|
||||
class MiddlewareNotUsed(Exception):
|
||||
"This middleware is not used in this server configuration"
|
||||
"""This middleware is not used in this server configuration"""
|
||||
pass
|
||||
|
||||
|
||||
class ImproperlyConfigured(Exception):
|
||||
"Django is somehow improperly configured"
|
||||
"""Django is somehow improperly configured"""
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ except (ImportError, AttributeError):
|
|||
|
||||
def fd(f):
|
||||
"""Get a filedescriptor from something which could be a file or an fd."""
|
||||
return hasattr(f, 'fileno') and f.fileno() or f
|
||||
return f.fileno() if hasattr(f, 'fileno') else f
|
||||
|
||||
if system_type == 'nt':
|
||||
def lock(file, flags):
|
||||
|
|
|
@ -62,7 +62,7 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove
|
|||
with open(old_file_name, 'rb') as old_file:
|
||||
# now open the new file, not forgetting allow_overwrite
|
||||
fd = os.open(new_file_name, os.O_WRONLY | os.O_CREAT | getattr(os, 'O_BINARY', 0) |
|
||||
(not allow_overwrite and os.O_EXCL or 0))
|
||||
(os.O_EXCL if not allow_overwrite else 0))
|
||||
try:
|
||||
locks.lock(fd, locks.LOCK_EX)
|
||||
current_chunk = None
|
||||
|
@ -77,8 +77,8 @@ def file_move_safe(old_file_name, new_file_name, chunk_size = 1024*64, allow_ove
|
|||
try:
|
||||
os.remove(old_file_name)
|
||||
except OSError as e:
|
||||
# Certain operating systems (Cygwin and Windows)
|
||||
# fail when deleting opened files, ignore it. (For the
|
||||
# Certain operating systems (Cygwin and Windows)
|
||||
# fail when deleting opened files, ignore it. (For the
|
||||
# systems where this happens, temporary files will be auto-deleted
|
||||
# on close anyway.)
|
||||
if getattr(e, 'winerror', 0) != 32 and getattr(e, 'errno', 0) != 13:
|
||||
|
|
|
@ -8,7 +8,7 @@ import itertools
|
|||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from django.core.files import locks, File
|
||||
from django.core.files.move import file_move_safe
|
||||
from django.utils.encoding import force_text, filepath_to_uri
|
||||
|
@ -260,7 +260,7 @@ class FileSystemStorage(Storage):
|
|||
try:
|
||||
path = safe_join(self.location, name)
|
||||
except ValueError:
|
||||
raise SuspiciousOperation("Attempted access to '%s' denied." % name)
|
||||
raise SuspiciousFileOperation("Attempted access to '%s' denied." % name)
|
||||
return os.path.normpath(path)
|
||||
|
||||
def size(self, name):
|
||||
|
|
|
@ -8,7 +8,7 @@ from django import http
|
|||
from django.conf import settings
|
||||
from django.core import urlresolvers
|
||||
from django.core import signals
|
||||
from django.core.exceptions import MiddlewareNotUsed, PermissionDenied
|
||||
from django.core.exceptions import MiddlewareNotUsed, PermissionDenied, SuspiciousOperation
|
||||
from django.db import connections, transaction
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.module_loading import import_by_path
|
||||
|
@ -66,10 +66,11 @@ class BaseHandler(object):
|
|||
self._request_middleware = request_middleware
|
||||
|
||||
def make_view_atomic(self, view):
|
||||
if getattr(view, 'transactions_per_request', True):
|
||||
for db in connections.all():
|
||||
if db.settings_dict['ATOMIC_REQUESTS']:
|
||||
view = transaction.atomic(using=db.alias)(view)
|
||||
non_atomic_requests = getattr(view, '_non_atomic_requests', set())
|
||||
for db in connections.all():
|
||||
if (db.settings_dict['ATOMIC_REQUESTS']
|
||||
and db.alias not in non_atomic_requests):
|
||||
view = transaction.atomic(using=db.alias)(view)
|
||||
return view
|
||||
|
||||
def get_response(self, request):
|
||||
|
@ -169,11 +170,27 @@ class BaseHandler(object):
|
|||
response = self.handle_uncaught_exception(request,
|
||||
resolver, sys.exc_info())
|
||||
|
||||
except SuspiciousOperation as e:
|
||||
# The request logger receives events for any problematic request
|
||||
# The security logger receives events for all SuspiciousOperations
|
||||
security_logger = logging.getLogger('django.security.%s' %
|
||||
e.__class__.__name__)
|
||||
security_logger.error(force_text(e))
|
||||
|
||||
try:
|
||||
callback, param_dict = resolver.resolve400()
|
||||
response = callback(request, **param_dict)
|
||||
except:
|
||||
signals.got_request_exception.send(
|
||||
sender=self.__class__, request=request)
|
||||
response = self.handle_uncaught_exception(request,
|
||||
resolver, sys.exc_info())
|
||||
|
||||
except SystemExit:
|
||||
# Allow sys.exit() to actually exit. See tickets #1023 and #4701
|
||||
raise
|
||||
|
||||
except: # Handle everything else, including SuspiciousOperation, etc.
|
||||
except: # Handle everything else.
|
||||
# Get the exception info now, in case another exception is thrown later.
|
||||
signals.got_request_exception.send(sender=self.__class__, request=request)
|
||||
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
|
||||
|
|
|
@ -13,67 +13,12 @@ from django.core.urlresolvers import set_script_prefix
|
|||
from django.utils import datastructures
|
||||
from django.utils.encoding import force_str, force_text, iri_to_uri
|
||||
|
||||
# For backwards compatibility -- lots of code uses this in the wild!
|
||||
from django.http.response import REASON_PHRASES as STATUS_CODE_TEXT
|
||||
|
||||
logger = logging.getLogger('django.request')
|
||||
|
||||
|
||||
# See http://www.iana.org/assignments/http-status-codes
|
||||
STATUS_CODE_TEXT = {
|
||||
100: 'CONTINUE',
|
||||
101: 'SWITCHING PROTOCOLS',
|
||||
102: 'PROCESSING',
|
||||
200: 'OK',
|
||||
201: 'CREATED',
|
||||
202: 'ACCEPTED',
|
||||
203: 'NON-AUTHORITATIVE INFORMATION',
|
||||
204: 'NO CONTENT',
|
||||
205: 'RESET CONTENT',
|
||||
206: 'PARTIAL CONTENT',
|
||||
207: 'MULTI-STATUS',
|
||||
208: 'ALREADY REPORTED',
|
||||
226: 'IM USED',
|
||||
300: 'MULTIPLE CHOICES',
|
||||
301: 'MOVED PERMANENTLY',
|
||||
302: 'FOUND',
|
||||
303: 'SEE OTHER',
|
||||
304: 'NOT MODIFIED',
|
||||
305: 'USE PROXY',
|
||||
306: 'RESERVED',
|
||||
307: 'TEMPORARY REDIRECT',
|
||||
400: 'BAD REQUEST',
|
||||
401: 'UNAUTHORIZED',
|
||||
402: 'PAYMENT REQUIRED',
|
||||
403: 'FORBIDDEN',
|
||||
404: 'NOT FOUND',
|
||||
405: 'METHOD NOT ALLOWED',
|
||||
406: 'NOT ACCEPTABLE',
|
||||
407: 'PROXY AUTHENTICATION REQUIRED',
|
||||
408: 'REQUEST TIMEOUT',
|
||||
409: 'CONFLICT',
|
||||
410: 'GONE',
|
||||
411: 'LENGTH REQUIRED',
|
||||
412: 'PRECONDITION FAILED',
|
||||
413: 'REQUEST ENTITY TOO LARGE',
|
||||
414: 'REQUEST-URI TOO LONG',
|
||||
415: 'UNSUPPORTED MEDIA TYPE',
|
||||
416: 'REQUESTED RANGE NOT SATISFIABLE',
|
||||
417: 'EXPECTATION FAILED',
|
||||
418: "I'M A TEAPOT",
|
||||
422: 'UNPROCESSABLE ENTITY',
|
||||
423: 'LOCKED',
|
||||
424: 'FAILED DEPENDENCY',
|
||||
426: 'UPGRADE REQUIRED',
|
||||
500: 'INTERNAL SERVER ERROR',
|
||||
501: 'NOT IMPLEMENTED',
|
||||
502: 'BAD GATEWAY',
|
||||
503: 'SERVICE UNAVAILABLE',
|
||||
504: 'GATEWAY TIMEOUT',
|
||||
505: 'HTTP VERSION NOT SUPPORTED',
|
||||
506: 'VARIANT ALSO NEGOTIATES',
|
||||
507: 'INSUFFICIENT STORAGE',
|
||||
508: 'LOOP DETECTED',
|
||||
510: 'NOT EXTENDED',
|
||||
}
|
||||
|
||||
class LimitedStream(object):
|
||||
'''
|
||||
LimitedStream wraps another stream in order to not allow reading from it
|
||||
|
@ -254,11 +199,7 @@ class WSGIHandler(base.BaseHandler):
|
|||
|
||||
response._handler_class = self.__class__
|
||||
|
||||
try:
|
||||
status_text = STATUS_CODE_TEXT[response.status_code]
|
||||
except KeyError:
|
||||
status_text = 'UNKNOWN STATUS CODE'
|
||||
status = '%s %s' % (response.status_code, status_text)
|
||||
status = '%s %s' % (response.status_code, response.reason_phrase)
|
||||
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
||||
for c in response.cookies.values():
|
||||
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
||||
|
|
|
@ -7,7 +7,6 @@ import os
|
|||
import sys
|
||||
|
||||
from optparse import make_option, OptionParser
|
||||
import traceback
|
||||
|
||||
import django
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
@ -171,7 +170,7 @@ class BaseCommand(object):
|
|||
make_option('--pythonpath',
|
||||
help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'),
|
||||
make_option('--traceback', action='store_true',
|
||||
help='Print traceback on exception'),
|
||||
help='Raise on exception'),
|
||||
)
|
||||
help = ''
|
||||
args = ''
|
||||
|
@ -231,7 +230,8 @@ class BaseCommand(object):
|
|||
Set up any environment changes requested (e.g., Python path
|
||||
and Django settings), then run this command. If the
|
||||
command raises a ``CommandError``, intercept it and print it sensibly
|
||||
to stderr.
|
||||
to stderr. If the ``--traceback`` option is present or the raised
|
||||
``Exception`` is not ``CommandError``, raise it.
|
||||
"""
|
||||
parser = self.create_parser(argv[0], argv[1])
|
||||
options, args = parser.parse_args(argv[2:])
|
||||
|
@ -239,12 +239,12 @@ class BaseCommand(object):
|
|||
try:
|
||||
self.execute(*args, **options.__dict__)
|
||||
except Exception as e:
|
||||
if options.traceback or not isinstance(e, CommandError):
|
||||
raise
|
||||
|
||||
# self.stderr is not guaranteed to be set here
|
||||
stderr = getattr(self, 'stderr', OutputWrapper(sys.stderr, self.style.ERROR))
|
||||
if options.traceback or not isinstance(e, CommandError):
|
||||
stderr.write(traceback.format_exc())
|
||||
else:
|
||||
stderr.write('%s: %s' % (e.__class__.__name__, e))
|
||||
stderr.write('%s: %s' % (e.__class__.__name__, e))
|
||||
sys.exit(1)
|
||||
|
||||
def execute(self, *args, **options):
|
||||
|
|
|
@ -38,7 +38,7 @@ class Command(LabelCommand):
|
|||
qn = connection.ops.quote_name
|
||||
for f in fields:
|
||||
field_output = [qn(f.name), f.db_type(connection=connection)]
|
||||
field_output.append("%sNULL" % (not f.null and "NOT " or ""))
|
||||
field_output.append("%sNULL" % ("NOT " if not f.null else ""))
|
||||
if f.primary_key:
|
||||
field_output.append("PRIMARY KEY")
|
||||
elif f.unique:
|
||||
|
@ -51,7 +51,7 @@ class Command(LabelCommand):
|
|||
table_output.append(" ".join(field_output))
|
||||
full_statement = ["CREATE TABLE %s (" % qn(tablename)]
|
||||
for i, line in enumerate(table_output):
|
||||
full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or ''))
|
||||
full_statement.append(' %s%s' % (line, ',' if i < len(table_output)-1 else ''))
|
||||
full_statement.append(');')
|
||||
with transaction.commit_on_success_unless_managed():
|
||||
curs = connection.cursor()
|
||||
|
|
|
@ -21,6 +21,9 @@ class Command(BaseCommand):
|
|||
help='Use natural keys if they are available.'),
|
||||
make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False,
|
||||
help="Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager."),
|
||||
make_option('--pks', dest='primary_keys', help="Only dump objects with "
|
||||
"given primary keys. Accepts a comma seperated list of keys. "
|
||||
"This option will only work when you specify one model."),
|
||||
)
|
||||
help = ("Output the contents of the database as a fixture of the given "
|
||||
"format (using each model's default manager unless --all is "
|
||||
|
@ -37,6 +40,12 @@ class Command(BaseCommand):
|
|||
show_traceback = options.get('traceback')
|
||||
use_natural_keys = options.get('use_natural_keys')
|
||||
use_base_manager = options.get('use_base_manager')
|
||||
pks = options.get('primary_keys')
|
||||
|
||||
if pks:
|
||||
primary_keys = pks.split(',')
|
||||
else:
|
||||
primary_keys = []
|
||||
|
||||
excluded_apps = set()
|
||||
excluded_models = set()
|
||||
|
@ -55,8 +64,12 @@ class Command(BaseCommand):
|
|||
raise CommandError('Unknown app in excludes: %s' % exclude)
|
||||
|
||||
if len(app_labels) == 0:
|
||||
if primary_keys:
|
||||
raise CommandError("You can only use --pks option with one model")
|
||||
app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps)
|
||||
else:
|
||||
if len(app_labels) > 1 and primary_keys:
|
||||
raise CommandError("You can only use --pks option with one model")
|
||||
app_list = SortedDict()
|
||||
for label in app_labels:
|
||||
try:
|
||||
|
@ -77,6 +90,8 @@ class Command(BaseCommand):
|
|||
else:
|
||||
app_list[app] = [model]
|
||||
except ValueError:
|
||||
if primary_keys:
|
||||
raise CommandError("You can only use --pks option with one model")
|
||||
# This is just an app - no model qualifier
|
||||
app_label = label
|
||||
try:
|
||||
|
@ -107,8 +122,11 @@ class Command(BaseCommand):
|
|||
objects = model._base_manager
|
||||
else:
|
||||
objects = model._default_manager
|
||||
for obj in objects.using(using).\
|
||||
order_by(model._meta.pk.name).iterator():
|
||||
|
||||
queryset = objects.using(using).order_by(model._meta.pk.name)
|
||||
if primary_keys:
|
||||
queryset = queryset.filter(pk__in=primary_keys)
|
||||
for obj in queryset.iterator():
|
||||
yield obj
|
||||
|
||||
try:
|
||||
|
|
|
@ -20,7 +20,7 @@ class Command(NoArgsCommand):
|
|||
default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. '
|
||||
'Defaults to the "default" database.'),
|
||||
make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True,
|
||||
help='Tells Django not to load any initial data after database synchronization.'),
|
||||
help='Tells Django not to load any initial data after database synchronization.'),
|
||||
)
|
||||
help = ('Returns the database to the state it was in immediately after '
|
||||
'syncdb was executed. This means that all data will be removed '
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import glob
|
||||
import gzip
|
||||
import os
|
||||
import zipfile
|
||||
from optparse import make_option
|
||||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import serializers
|
||||
|
@ -11,8 +13,9 @@ from django.core.management.base import BaseCommand, CommandError
|
|||
from django.core.management.color import no_style
|
||||
from django.db import (connections, router, transaction, DEFAULT_DB_ALIAS,
|
||||
IntegrityError, DatabaseError)
|
||||
from django.db.models import get_apps
|
||||
from django.db.models import get_app_paths
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import cached_property, memoize
|
||||
from django.utils._os import upath
|
||||
from itertools import product
|
||||
|
||||
|
@ -43,9 +46,8 @@ class Command(BaseCommand):
|
|||
|
||||
if not len(fixture_labels):
|
||||
raise CommandError(
|
||||
"No database fixture specified. Please provide the path of at "
|
||||
"least one fixture in the command line."
|
||||
)
|
||||
"No database fixture specified. Please provide the path "
|
||||
"of at least one fixture in the command line.")
|
||||
|
||||
self.verbosity = int(options.get('verbosity'))
|
||||
|
||||
|
@ -68,37 +70,18 @@ class Command(BaseCommand):
|
|||
self.fixture_object_count = 0
|
||||
self.models = set()
|
||||
|
||||
class SingleZipReader(zipfile.ZipFile):
|
||||
def __init__(self, *args, **kwargs):
|
||||
zipfile.ZipFile.__init__(self, *args, **kwargs)
|
||||
if settings.DEBUG:
|
||||
assert len(self.namelist()) == 1, "Zip-compressed fixtures must contain only one file."
|
||||
def read(self):
|
||||
return zipfile.ZipFile.read(self, self.namelist()[0])
|
||||
|
||||
self.compression_types = {
|
||||
self.serialization_formats = serializers.get_public_serializer_formats()
|
||||
self.compression_formats = {
|
||||
None: open,
|
||||
'gz': gzip.GzipFile,
|
||||
'zip': SingleZipReader
|
||||
}
|
||||
if has_bz2:
|
||||
self.compression_types['bz2'] = bz2.BZ2File
|
||||
|
||||
app_module_paths = []
|
||||
for app in get_apps():
|
||||
if hasattr(app, '__path__'):
|
||||
# It's a 'models/' subpackage
|
||||
for path in app.__path__:
|
||||
app_module_paths.append(upath(path))
|
||||
else:
|
||||
# It's a models.py module
|
||||
app_module_paths.append(upath(app.__file__))
|
||||
|
||||
app_fixtures = [os.path.join(os.path.dirname(path), 'fixtures') for path in app_module_paths]
|
||||
self.compression_formats['bz2'] = bz2.BZ2File
|
||||
|
||||
with connection.constraint_checks_disabled():
|
||||
for fixture_label in fixture_labels:
|
||||
self.load_label(fixture_label, app_fixtures)
|
||||
self.load_label(fixture_label)
|
||||
|
||||
# Since we disabled constraint checks, we must manually check for
|
||||
# any invalid keys that might have been added
|
||||
|
@ -123,122 +106,174 @@ class Command(BaseCommand):
|
|||
|
||||
if self.verbosity >= 1:
|
||||
if self.fixture_object_count == self.loaded_object_count:
|
||||
self.stdout.write("Installed %d object(s) from %d fixture(s)" % (
|
||||
self.loaded_object_count, self.fixture_count))
|
||||
self.stdout.write("Installed %d object(s) from %d fixture(s)" %
|
||||
(self.loaded_object_count, self.fixture_count))
|
||||
else:
|
||||
self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" % (
|
||||
self.loaded_object_count, self.fixture_object_count, self.fixture_count))
|
||||
self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" %
|
||||
(self.loaded_object_count, self.fixture_object_count, self.fixture_count))
|
||||
|
||||
def load_label(self, fixture_label, app_fixtures):
|
||||
def load_label(self, fixture_label):
|
||||
"""
|
||||
Loads fixtures files for a given label.
|
||||
"""
|
||||
for fixture_file, fixture_dir, fixture_name in self.find_fixtures(fixture_label):
|
||||
_, ser_fmt, cmp_fmt = self.parse_name(os.path.basename(fixture_file))
|
||||
open_method = self.compression_formats[cmp_fmt]
|
||||
fixture = open_method(fixture_file, 'r')
|
||||
try:
|
||||
self.fixture_count += 1
|
||||
objects_in_fixture = 0
|
||||
loaded_objects_in_fixture = 0
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Installing %s fixture '%s' from %s." %
|
||||
(ser_fmt, fixture_name, humanize(fixture_dir)))
|
||||
|
||||
parts = fixture_label.split('.')
|
||||
objects = serializers.deserialize(ser_fmt, fixture,
|
||||
using=self.using, ignorenonexistent=self.ignore)
|
||||
|
||||
if len(parts) > 1 and parts[-1] in self.compression_types:
|
||||
compression_formats = [parts[-1]]
|
||||
parts = parts[:-1]
|
||||
else:
|
||||
compression_formats = self.compression_types.keys()
|
||||
for obj in objects:
|
||||
objects_in_fixture += 1
|
||||
if router.allow_syncdb(self.using, obj.object.__class__):
|
||||
loaded_objects_in_fixture += 1
|
||||
self.models.add(obj.object.__class__)
|
||||
try:
|
||||
obj.save(using=self.using)
|
||||
except (DatabaseError, IntegrityError) as e:
|
||||
e.args = ("Could not load %(app_label)s.%(object_name)s(pk=%(pk)s): %(error_msg)s" % {
|
||||
'app_label': obj.object._meta.app_label,
|
||||
'object_name': obj.object._meta.object_name,
|
||||
'pk': obj.object.pk,
|
||||
'error_msg': force_text(e)
|
||||
},)
|
||||
raise
|
||||
|
||||
if len(parts) == 1:
|
||||
fixture_name = parts[0]
|
||||
formats = serializers.get_public_serializer_formats()
|
||||
else:
|
||||
fixture_name, format = '.'.join(parts[:-1]), parts[-1]
|
||||
if format in serializers.get_public_serializer_formats():
|
||||
formats = [format]
|
||||
else:
|
||||
formats = []
|
||||
self.loaded_object_count += loaded_objects_in_fixture
|
||||
self.fixture_object_count += objects_in_fixture
|
||||
except Exception as e:
|
||||
if not isinstance(e, CommandError):
|
||||
e.args = ("Problem installing fixture '%s': %s" % (fixture_file, e),)
|
||||
raise
|
||||
finally:
|
||||
fixture.close()
|
||||
|
||||
if formats:
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Loading '%s' fixtures..." % fixture_name)
|
||||
else:
|
||||
# If the fixture we loaded contains 0 objects, assume that an
|
||||
# error was encountered during fixture loading.
|
||||
if objects_in_fixture == 0:
|
||||
raise CommandError(
|
||||
"No fixture data found for '%s'. "
|
||||
"(File format may be invalid.)" % fixture_name)
|
||||
|
||||
def _find_fixtures(self, fixture_label):
|
||||
"""
|
||||
Finds fixture files for a given label.
|
||||
"""
|
||||
fixture_name, ser_fmt, cmp_fmt = self.parse_name(fixture_label)
|
||||
databases = [self.using, None]
|
||||
cmp_fmts = list(self.compression_formats.keys()) if cmp_fmt is None else [cmp_fmt]
|
||||
ser_fmts = serializers.get_public_serializer_formats() if ser_fmt is None else [ser_fmt]
|
||||
|
||||
# Check kept for backwards-compatibility; it doesn't look very useful.
|
||||
if '.' in os.path.basename(fixture_name):
|
||||
raise CommandError(
|
||||
"Problem installing fixture '%s': %s is not a known serialization format." %
|
||||
(fixture_name, format))
|
||||
|
||||
if os.path.isabs(fixture_name):
|
||||
fixture_dirs = [fixture_name]
|
||||
else:
|
||||
fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + ['']
|
||||
|
||||
for fixture_dir in fixture_dirs:
|
||||
self.process_dir(fixture_dir, fixture_name, compression_formats,
|
||||
formats)
|
||||
|
||||
def process_dir(self, fixture_dir, fixture_name, compression_formats,
|
||||
serialization_formats):
|
||||
|
||||
humanize = lambda dirname: "'%s'" % dirname if dirname else 'absolute path'
|
||||
"Problem installing fixture '%s': %s is not a known "
|
||||
"serialization format." % tuple(fixture_name.rsplit('.')))
|
||||
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir))
|
||||
self.stdout.write("Loading '%s' fixtures..." % fixture_name)
|
||||
|
||||
label_found = False
|
||||
for combo in product([self.using, None], serialization_formats, compression_formats):
|
||||
database, format, compression_format = combo
|
||||
file_name = '.'.join(
|
||||
p for p in [
|
||||
fixture_name, database, format, compression_format
|
||||
]
|
||||
if p
|
||||
)
|
||||
if os.path.isabs(fixture_name):
|
||||
fixture_dirs = [os.path.dirname(fixture_name)]
|
||||
fixture_name = os.path.basename(fixture_name)
|
||||
else:
|
||||
fixture_dirs = self.fixture_dirs
|
||||
|
||||
if self.verbosity >= 3:
|
||||
self.stdout.write("Trying %s for %s fixture '%s'..." % \
|
||||
(humanize(fixture_dir), file_name, fixture_name))
|
||||
full_path = os.path.join(fixture_dir, file_name)
|
||||
open_method = self.compression_types[compression_format]
|
||||
try:
|
||||
fixture = open_method(full_path, 'r')
|
||||
except IOError:
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("No %s fixture '%s' in %s." % \
|
||||
(format, fixture_name, humanize(fixture_dir)))
|
||||
else:
|
||||
try:
|
||||
if label_found:
|
||||
raise CommandError("Multiple fixtures named '%s' in %s. Aborting." %
|
||||
(fixture_name, humanize(fixture_dir)))
|
||||
suffixes = ('.'.join(ext for ext in combo if ext)
|
||||
for combo in product(databases, ser_fmts, cmp_fmts))
|
||||
targets = set('.'.join((fixture_name, suffix)) for suffix in suffixes)
|
||||
|
||||
self.fixture_count += 1
|
||||
objects_in_fixture = 0
|
||||
loaded_objects_in_fixture = 0
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Installing %s fixture '%s' from %s." % \
|
||||
(format, fixture_name, humanize(fixture_dir)))
|
||||
fixture_files = []
|
||||
for fixture_dir in fixture_dirs:
|
||||
if self.verbosity >= 2:
|
||||
self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir))
|
||||
fixture_files_in_dir = []
|
||||
for candidate in glob.iglob(os.path.join(fixture_dir, fixture_name + '*')):
|
||||
if os.path.basename(candidate) in targets:
|
||||
# Save the fixture_dir and fixture_name for future error messages.
|
||||
fixture_files_in_dir.append((candidate, fixture_dir, fixture_name))
|
||||
|
||||
objects = serializers.deserialize(format, fixture, using=self.using, ignorenonexistent=self.ignore)
|
||||
if self.verbosity >= 2 and not fixture_files_in_dir:
|
||||
self.stdout.write("No fixture '%s' in %s." %
|
||||
(fixture_name, humanize(fixture_dir)))
|
||||
|
||||
for obj in objects:
|
||||
objects_in_fixture += 1
|
||||
if router.allow_syncdb(self.using, obj.object.__class__):
|
||||
loaded_objects_in_fixture += 1
|
||||
self.models.add(obj.object.__class__)
|
||||
try:
|
||||
obj.save(using=self.using)
|
||||
except (DatabaseError, IntegrityError) as e:
|
||||
e.args = ("Could not load %(app_label)s.%(object_name)s(pk=%(pk)s): %(error_msg)s" % {
|
||||
'app_label': obj.object._meta.app_label,
|
||||
'object_name': obj.object._meta.object_name,
|
||||
'pk': obj.object.pk,
|
||||
'error_msg': force_text(e)
|
||||
},)
|
||||
raise
|
||||
# Check kept for backwards-compatibility; it isn't clear why
|
||||
# duplicates are only allowed in different directories.
|
||||
if len(fixture_files_in_dir) > 1:
|
||||
raise CommandError(
|
||||
"Multiple fixtures named '%s' in %s. Aborting." %
|
||||
(fixture_name, humanize(fixture_dir)))
|
||||
fixture_files.extend(fixture_files_in_dir)
|
||||
|
||||
self.loaded_object_count += loaded_objects_in_fixture
|
||||
self.fixture_object_count += objects_in_fixture
|
||||
label_found = True
|
||||
except Exception as e:
|
||||
if not isinstance(e, CommandError):
|
||||
e.args = ("Problem installing fixture '%s': %s" % (full_path, e),)
|
||||
raise
|
||||
finally:
|
||||
fixture.close()
|
||||
if fixture_name != 'initial_data' and not fixture_files:
|
||||
# Warning kept for backwards-compatibility; why not an exception?
|
||||
warnings.warn("No fixture named '%s' found." % fixture_name)
|
||||
|
||||
# If the fixture we loaded contains 0 objects, assume that an
|
||||
# error was encountered during fixture loading.
|
||||
if objects_in_fixture == 0:
|
||||
raise CommandError(
|
||||
"No fixture data found for '%s'. (File format may be invalid.)" %
|
||||
(fixture_name))
|
||||
return fixture_files
|
||||
|
||||
_label_to_fixtures_cache = {}
|
||||
find_fixtures = memoize(_find_fixtures, _label_to_fixtures_cache, 2)
|
||||
|
||||
@cached_property
|
||||
def fixture_dirs(self):
|
||||
"""
|
||||
Return a list of fixture directories.
|
||||
|
||||
The list contains the 'fixtures' subdirectory of each installed
|
||||
application, if it exists, the directories in FIXTURE_DIRS, and the
|
||||
current directory.
|
||||
"""
|
||||
dirs = []
|
||||
for path in get_app_paths():
|
||||
d = os.path.join(os.path.dirname(path), 'fixtures')
|
||||
if os.path.isdir(d):
|
||||
dirs.append(d)
|
||||
dirs.extend(list(settings.FIXTURE_DIRS))
|
||||
dirs.append('')
|
||||
dirs = [upath(os.path.abspath(os.path.realpath(d))) for d in dirs]
|
||||
return dirs
|
||||
|
||||
def parse_name(self, fixture_name):
|
||||
"""
|
||||
Splits fixture name in name, serialization format, compression format.
|
||||
"""
|
||||
parts = fixture_name.rsplit('.', 2)
|
||||
|
||||
if len(parts) > 1 and parts[-1] in self.compression_formats:
|
||||
cmp_fmt = parts[-1]
|
||||
parts = parts[:-1]
|
||||
else:
|
||||
cmp_fmt = None
|
||||
|
||||
if len(parts) > 1 and parts[-1] in self.serialization_formats:
|
||||
ser_fmt = parts[-1]
|
||||
parts = parts[:-1]
|
||||
else:
|
||||
ser_fmt = None
|
||||
|
||||
name = '.'.join(parts)
|
||||
|
||||
return name, ser_fmt, cmp_fmt
|
||||
|
||||
|
||||
class SingleZipReader(zipfile.ZipFile):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
zipfile.ZipFile.__init__(self, *args, **kwargs)
|
||||
if len(self.namelist()) != 1:
|
||||
raise ValueError("Zip-compressed fixtures must contain one file.")
|
||||
|
||||
def read(self):
|
||||
return zipfile.ZipFile.read(self, self.namelist()[0])
|
||||
|
||||
|
||||
def humanize(dirname):
|
||||
return "'%s'" % dirname if dirname else 'absolute path'
|
||||
|
|
|
@ -250,18 +250,6 @@ class Command(NoArgsCommand):
|
|||
"if you want to enable i18n for your project or application.")
|
||||
|
||||
check_programs('xgettext')
|
||||
# We require gettext version 0.15 or newer.
|
||||
output, errors, status = popen_wrapper(['xgettext', '--version'])
|
||||
if status != STATUS_OK:
|
||||
raise CommandError("Error running xgettext. Note that Django "
|
||||
"internationalization requires GNU gettext 0.15 or newer.")
|
||||
match = re.search(r'(?P<major>\d+)\.(?P<minor>\d+)', output)
|
||||
if match:
|
||||
xversion = (int(match.group('major')), int(match.group('minor')))
|
||||
if xversion < (0, 15):
|
||||
raise CommandError("Django internationalization requires GNU "
|
||||
"gettext 0.15 or newer. You are using version %s, please "
|
||||
"upgrade your gettext toolset." % match.group())
|
||||
|
||||
potfile = self.build_pot_file(localedir)
|
||||
|
||||
|
@ -309,10 +297,9 @@ class Command(NoArgsCommand):
|
|||
"""
|
||||
Check if the given path should be ignored or not.
|
||||
"""
|
||||
for pattern in ignore_patterns:
|
||||
if fnmatch.fnmatchcase(path, pattern):
|
||||
return True
|
||||
return False
|
||||
filename = os.path.basename(path)
|
||||
ignore = lambda pattern: fnmatch.fnmatchcase(filename, pattern)
|
||||
return any(ignore(pattern) for pattern in ignore_patterns)
|
||||
|
||||
dir_suffix = '%s*' % os.sep
|
||||
norm_patterns = [p[:-len(dir_suffix)] if p.endswith(dir_suffix) else p for p in self.ignore_patterns]
|
||||
|
|
|
@ -99,7 +99,7 @@ class Command(BaseCommand):
|
|||
"started_at": datetime.now().strftime('%B %d, %Y - %X'),
|
||||
"version": self.get_version(),
|
||||
"settings": settings.SETTINGS_MODULE,
|
||||
"addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr,
|
||||
"addr": '[%s]' % self.addr if self._raw_ipv6 else self.addr,
|
||||
"port": self.port,
|
||||
"quit_command": quit_command,
|
||||
})
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
from optparse import make_option
|
||||
import itertools
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.core.management.color import no_style
|
||||
from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal
|
||||
from django.core.management.sql import custom_sql_for_model, emit_post_sync_signal, emit_pre_sync_signal
|
||||
from django.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.importlib import import_module
|
||||
|
@ -82,6 +83,9 @@ class Command(NoArgsCommand):
|
|||
for app_name, model_list in all_models
|
||||
)
|
||||
|
||||
create_models = set([x for x in itertools.chain(*manifest.values())])
|
||||
emit_pre_sync_signal(create_models, verbosity, interactive, db)
|
||||
|
||||
# Create the tables for each model
|
||||
if verbosity >= 1:
|
||||
self.stdout.write("Creating tables ...\n")
|
||||
|
|
|
@ -29,7 +29,7 @@ class Command(BaseCommand):
|
|||
)
|
||||
help = ('Runs the test suite for the specified applications, or the '
|
||||
'entire site if no apps are specified.')
|
||||
args = '[appname ...]'
|
||||
args = '[appname|appname.tests.TestCase|appname.tests.TestCase.test_method]...'
|
||||
|
||||
requires_model_validation = False
|
||||
|
||||
|
|
|
@ -133,14 +133,15 @@ def sql_custom(app, style, connection):
|
|||
def sql_indexes(app, style, connection):
|
||||
"Returns a list of the CREATE INDEX SQL statements for all models in the given app."
|
||||
output = []
|
||||
for model in models.get_models(app):
|
||||
for model in models.get_models(app, include_auto_created=True):
|
||||
output.extend(connection.creation.sql_indexes_for_model(model, style))
|
||||
return output
|
||||
|
||||
|
||||
def sql_destroy_indexes(app, style, connection):
|
||||
"Returns a list of the DROP INDEX SQL statements for all models in the given app."
|
||||
output = []
|
||||
for model in models.get_models(app):
|
||||
for model in models.get_models(app, include_auto_created=True):
|
||||
output.extend(connection.creation.sql_destroy_indexes_for_model(model, style))
|
||||
return output
|
||||
|
||||
|
@ -191,6 +192,19 @@ def custom_sql_for_model(model, style, connection):
|
|||
return output
|
||||
|
||||
|
||||
def emit_pre_sync_signal(create_models, verbosity, interactive, db):
|
||||
# Emit the pre_sync signal for every application.
|
||||
for app in models.get_apps():
|
||||
app_name = app.__name__.split('.')[-2]
|
||||
if verbosity >= 2:
|
||||
print("Running pre-sync handlers for application %s" % app_name)
|
||||
models.signals.pre_syncdb.send(sender=app, app=app,
|
||||
create_models=create_models,
|
||||
verbosity=verbosity,
|
||||
interactive=interactive,
|
||||
db=db)
|
||||
|
||||
|
||||
def emit_post_sync_signal(created_models, verbosity, interactive, db):
|
||||
# Emit the post_sync signal for every application.
|
||||
for app in models.get_apps():
|
||||
|
|
|
@ -113,13 +113,15 @@ def get_validation_errors(outfile, app=None):
|
|||
e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name)
|
||||
if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders):
|
||||
e.add(opts, '"%s": FilePathFields must have either allow_files or allow_folders set to True.' % f.name)
|
||||
if isinstance(f, models.GenericIPAddressField) and not getattr(f, 'null', False) and getattr(f, 'blank', False):
|
||||
e.add(opts, '"%s": GenericIPAddressField can not accept blank values if null values are not allowed, as blank values are stored as null.' % f.name)
|
||||
if f.choices:
|
||||
if isinstance(f.choices, six.string_types) or not is_iterable(f.choices):
|
||||
e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name)
|
||||
else:
|
||||
for c in f.choices:
|
||||
if not isinstance(c, (list, tuple)) or len(c) != 2:
|
||||
e.add(opts, '"%s": "choices" should be a sequence of two-tuples.' % f.name)
|
||||
if isinstance(c, six.string_types) or not is_iterable(c) or len(c) != 2:
|
||||
e.add(opts, '"%s": "choices" should be a sequence of two-item iterables (e.g. list of 2 item tuples).' % f.name)
|
||||
if f.db_index not in (None, True, False):
|
||||
e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name)
|
||||
|
||||
|
|
|
@ -121,7 +121,9 @@ class Page(collections.Sequence):
|
|||
raise TypeError
|
||||
# The object_list is converted to a list so that if it was a QuerySet
|
||||
# it won't be a database hit per __getitem__.
|
||||
return list(self.object_list)[index]
|
||||
if not isinstance(self.object_list, list):
|
||||
self.object_list = list(self.object_list)
|
||||
return self.object_list[index]
|
||||
|
||||
def has_next(self):
|
||||
return self.number < self.paginator.num_pages
|
||||
|
|
|
@ -75,8 +75,7 @@ class Resolver404(Http404):
|
|||
pass
|
||||
|
||||
class NoReverseMatch(Exception):
|
||||
# Don't make this raise an error when used in a template.
|
||||
silent_variable_failure = True
|
||||
pass
|
||||
|
||||
def get_callable(lookup_view, can_fail=False):
|
||||
"""
|
||||
|
@ -360,6 +359,9 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
callback = getattr(urls, 'handler%s' % view_type)
|
||||
return get_callable(callback), {}
|
||||
|
||||
def resolve400(self):
|
||||
return self._resolve_special('400')
|
||||
|
||||
def resolve403(self):
|
||||
return self._resolve_special('403')
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
"""
|
||||
Pages in Django can are served up with custom HTTP headers containing useful
|
||||
information about those pages -- namely, the content type and object ID.
|
||||
|
||||
This module contains utility functions for retrieving and doing interesting
|
||||
things with these special "X-Headers" (so called because the HTTP spec demands
|
||||
that custom headers are prefixed with "X-").
|
||||
|
||||
Next time you're at slashdot.org, watch out for X-Fry and X-Bender. :)
|
||||
"""
|
||||
|
||||
def populate_xheaders(request, response, model, object_id):
|
||||
"""
|
||||
Adds the "X-Object-Type" and "X-Object-Id" headers to the given
|
||||
HttpResponse according to the given model and object_id -- but only if the
|
||||
given HttpRequest object has an IP address within the INTERNAL_IPS setting
|
||||
or if the request is from a logged in staff member.
|
||||
"""
|
||||
from django.conf import settings
|
||||
if (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS
|
||||
or (hasattr(request, 'user') and request.user.is_active
|
||||
and request.user.is_staff)):
|
||||
response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.model_name)
|
||||
response['X-Object-Id'] = str(object_id)
|
|
@ -1,24 +1,19 @@
|
|||
import warnings
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import signals
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db.utils import (DEFAULT_DB_ALIAS,
|
||||
DataError, OperationalError, IntegrityError, InternalError,
|
||||
ProgrammingError, NotSupportedError, DatabaseError,
|
||||
InterfaceError, Error,
|
||||
load_backend, ConnectionHandler, ConnectionRouter)
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
|
||||
'IntegrityError', 'DEFAULT_DB_ALIAS')
|
||||
|
||||
connections = ConnectionHandler()
|
||||
|
||||
if settings.DATABASES and DEFAULT_DB_ALIAS not in settings.DATABASES:
|
||||
raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)
|
||||
|
||||
connections = ConnectionHandler(settings.DATABASES)
|
||||
|
||||
router = ConnectionRouter(settings.DATABASE_ROUTERS)
|
||||
router = ConnectionRouter()
|
||||
|
||||
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
|
||||
# for backend bits.
|
||||
|
@ -45,7 +40,28 @@ class DefaultConnectionProxy(object):
|
|||
return delattr(connections[DEFAULT_DB_ALIAS], name)
|
||||
|
||||
connection = DefaultConnectionProxy()
|
||||
backend = load_backend(connection.settings_dict['ENGINE'])
|
||||
|
||||
class DefaultBackendProxy(object):
|
||||
"""
|
||||
Temporary proxy class used during deprecation period of the `backend` module
|
||||
variable.
|
||||
"""
|
||||
@cached_property
|
||||
def _backend(self):
|
||||
warnings.warn("Accessing django.db.backend is deprecated.",
|
||||
PendingDeprecationWarning, stacklevel=2)
|
||||
return load_backend(connections[DEFAULT_DB_ALIAS].settings_dict['ENGINE'])
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._backend, item)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
return setattr(self._backend, name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
return delattr(self._backend, name)
|
||||
|
||||
backend = DefaultBackendProxy()
|
||||
|
||||
def close_connection(**kwargs):
|
||||
warnings.warn(
|
||||
|
|
|
@ -390,9 +390,10 @@ class BaseDatabaseWrapper(object):
|
|||
def disable_constraint_checking(self):
|
||||
"""
|
||||
Backends can implement as needed to temporarily disable foreign key
|
||||
constraint checking.
|
||||
constraint checking. Should return True if the constraints were
|
||||
disabled and will need to be reenabled.
|
||||
"""
|
||||
pass
|
||||
return False
|
||||
|
||||
def enable_constraint_checking(self):
|
||||
"""
|
||||
|
@ -784,12 +785,12 @@ class BaseDatabaseOperations(object):
|
|||
"""
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def field_cast_sql(self, db_type):
|
||||
def field_cast_sql(self, db_type, internal_type):
|
||||
"""
|
||||
Given a column type (e.g. 'BLOB', 'VARCHAR'), returns the SQL necessary
|
||||
to cast it before using it in a WHERE statement. Note that the
|
||||
resulting string should contain a '%s' placeholder for the column being
|
||||
searched against.
|
||||
Given a column type (e.g. 'BLOB', 'VARCHAR'), and an internal type
|
||||
(e.g. 'GenericIPAddressField'), returns the SQL necessary to cast it
|
||||
before using it in a WHERE statement. Note that the resulting string
|
||||
should contain a '%s' placeholder for the column being searched against.
|
||||
"""
|
||||
return '%s'
|
||||
|
||||
|
|
|
@ -99,7 +99,7 @@ class BaseDatabaseCreation(object):
|
|||
style.SQL_TABLE(qn(opts.db_table)) + ' (']
|
||||
for i, line in enumerate(table_output): # Combine and add commas.
|
||||
full_statement.append(
|
||||
' %s%s' % (line, i < len(table_output) - 1 and ',' or ''))
|
||||
' %s%s' % (line, ',' if i < len(table_output) - 1 else ''))
|
||||
full_statement.append(')')
|
||||
if opts.db_tablespace:
|
||||
tablespace_sql = self.connection.ops.tablespace_sql(
|
||||
|
|
|
@ -17,8 +17,7 @@ class SQLCompiler(compiler.SQLCompiler):
|
|||
values.append(value)
|
||||
return row[:index_extra_select] + tuple(values)
|
||||
|
||||
def as_subquery_condition(self, alias, columns):
|
||||
qn = self.quote_name_unless_alias
|
||||
def as_subquery_condition(self, alias, columns, qn):
|
||||
qn2 = self.connection.ops.quote_name
|
||||
sql, params = self.as_sql()
|
||||
return '(%s) IN (%s)' % (', '.join(['%s.%s' % (qn(alias), qn2(column)) for column in columns]), sql), params
|
||||
|
|
|
@ -44,6 +44,11 @@ except ImportError as e:
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e)
|
||||
|
||||
try:
|
||||
import pytz
|
||||
except ImportError:
|
||||
pytz = None
|
||||
|
||||
from django.db import utils
|
||||
from django.db.backends import *
|
||||
from django.db.backends.oracle.client import DatabaseClient
|
||||
|
@ -78,6 +83,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
supports_subqueries_in_group_by = False
|
||||
supports_transactions = True
|
||||
supports_timezones = False
|
||||
has_zoneinfo_database = pytz is not None
|
||||
supports_bitwise_or = False
|
||||
can_defer_constraint_checks = True
|
||||
ignores_nulls_in_unique_constraints = False
|
||||
|
@ -243,9 +249,6 @@ WHEN (new.%(col_name)s IS NULL)
|
|||
value = value.date()
|
||||
return value
|
||||
|
||||
def datetime_cast_sql(self):
|
||||
return "TO_TIMESTAMP(%s, 'YYYY-MM-DD HH24:MI:SS.FF')"
|
||||
|
||||
def deferrable_sql(self):
|
||||
return " DEFERRABLE INITIALLY DEFERRED"
|
||||
|
||||
|
@ -255,7 +258,7 @@ WHEN (new.%(col_name)s IS NULL)
|
|||
def fetch_returned_insert_id(self, cursor):
|
||||
return int(cursor._insert_id_var.getvalue())
|
||||
|
||||
def field_cast_sql(self, db_type):
|
||||
def field_cast_sql(self, db_type, internal_type):
|
||||
if db_type and db_type.endswith('LOB'):
|
||||
return "DBMS_LOB.SUBSTR(%s)"
|
||||
else:
|
||||
|
@ -434,6 +437,17 @@ WHEN (new.%(col_name)s IS NULL)
|
|||
second = '%s-12-31'
|
||||
return [first % value, second % value]
|
||||
|
||||
def year_lookup_bounds_for_datetime_field(self, value):
|
||||
# The default implementation uses datetime objects for the bounds.
|
||||
# This must be overridden here, to use a formatted date (string) as
|
||||
# 'second' instead -- cx_Oracle chops the fraction-of-second part
|
||||
# off of datetime objects, leaving almost an entire second out of
|
||||
# the year under the default implementation.
|
||||
bounds = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value)
|
||||
if settings.USE_TZ:
|
||||
bounds = [b.astimezone(timezone.utc).replace(tzinfo=None) for b in bounds]
|
||||
return [b.isoformat(b' ') for b in bounds]
|
||||
|
||||
def combine_expression(self, connector, sub_expressions):
|
||||
"Oracle requires special cases for %% and & operators in query expressions"
|
||||
if connector == '%%':
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue