mirror of
https://github.com/django/django.git
synced 2025-03-13 02:40:47 +00:00
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
|
* Luke Plant
|
||||||
* Russell Keith-Magee
|
* Russell Keith-Magee
|
||||||
* Robert Wittams
|
* Robert Wittams
|
||||||
|
* James Bennett
|
||||||
* Gary Wilson
|
* Gary Wilson
|
||||||
|
* Matt Boersma
|
||||||
|
* Ian Kelly
|
||||||
|
* Joseph Kocherhans
|
||||||
* Brian Rosner
|
* Brian Rosner
|
||||||
* Justin Bronn
|
* Justin Bronn
|
||||||
* Karen Tracey
|
* Karen Tracey
|
||||||
* Jannis Leidel
|
* Jannis Leidel
|
||||||
* James Tauber
|
* James Tauber
|
||||||
* Alex Gaynor
|
* Alex Gaynor
|
||||||
|
* Simon Meers
|
||||||
* Andrew Godwin
|
* Andrew Godwin
|
||||||
* Carl Meyer
|
* Carl Meyer
|
||||||
* Ramiro Morales
|
* Ramiro Morales
|
||||||
|
* Gabriel Hurley
|
||||||
* Chris Beaven
|
* Chris Beaven
|
||||||
* Honza Král
|
* Honza Král
|
||||||
|
* Tim Graham
|
||||||
* Idan Gazit
|
* Idan Gazit
|
||||||
* Paul McMillan
|
* Paul McMillan
|
||||||
* Julien Phalip
|
* Julien Phalip
|
||||||
@ -36,6 +43,7 @@ The PRIMARY AUTHORS are (and/or have been):
|
|||||||
* Preston Holmes
|
* Preston Holmes
|
||||||
* Simon Charette
|
* Simon Charette
|
||||||
* Donald Stufft
|
* Donald Stufft
|
||||||
|
* Daniel Lindsley
|
||||||
* Marc Tamlyn
|
* Marc Tamlyn
|
||||||
|
|
||||||
More information on the main contributors to Django can be found in
|
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>
|
Randy Barlow <randy@electronsweatshop.com>
|
||||||
Scott Barr <scott@divisionbyzero.com.au>
|
Scott Barr <scott@divisionbyzero.com.au>
|
||||||
Jiri Barton
|
Jiri Barton
|
||||||
|
Jorge Bastida <me@jorgebastida.com>
|
||||||
Ned Batchelder <http://www.nedbatchelder.com/>
|
Ned Batchelder <http://www.nedbatchelder.com/>
|
||||||
batiste@dosimple.ch
|
batiste@dosimple.ch
|
||||||
Batman
|
Batman
|
||||||
Brian Beck <http://blog.brianbeck.com/>
|
Brian Beck <http://blog.brianbeck.com/>
|
||||||
Shannon -jj Behrens <http://jjinux.blogspot.com/>
|
Shannon -jj Behrens <http://jjinux.blogspot.com/>
|
||||||
Esdras Beleza <linux@esdrasbeleza.com>
|
Esdras Beleza <linux@esdrasbeleza.com>
|
||||||
|
Božidar Benko <bbenko@gmail.com>
|
||||||
Chris Bennett <chrisrbennett@yahoo.com>
|
Chris Bennett <chrisrbennett@yahoo.com>
|
||||||
James Bennett
|
|
||||||
Danilo Bargen
|
Danilo Bargen
|
||||||
Shai Berger <shai@platonix.com>
|
Shai Berger <shai@platonix.com>
|
||||||
berto
|
berto
|
||||||
@ -102,9 +111,9 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Paul Bissex <http://e-scribe.com/>
|
Paul Bissex <http://e-scribe.com/>
|
||||||
Loïc Bistuer <loic.bistuer@sixmedia.com>
|
Loïc Bistuer <loic.bistuer@sixmedia.com>
|
||||||
Simon Blanchard
|
Simon Blanchard
|
||||||
|
Jérémie Blaser <blaserje@gmail.com>
|
||||||
Craig Blaszczyk <masterjakul@gmail.com>
|
Craig Blaszczyk <masterjakul@gmail.com>
|
||||||
David Blewett <david@dawninglight.net>
|
David Blewett <david@dawninglight.net>
|
||||||
Matt Boersma <matt@sprout.org>
|
|
||||||
Artem Gnilov <boobsd@gmail.com>
|
Artem Gnilov <boobsd@gmail.com>
|
||||||
Matías Bordese
|
Matías Bordese
|
||||||
Nate Bragg <jonathan.bragg@alum.rpi.edu>
|
Nate Bragg <jonathan.bragg@alum.rpi.edu>
|
||||||
@ -117,6 +126,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
bthomas
|
bthomas
|
||||||
btoll@bestweb.net
|
btoll@bestweb.net
|
||||||
Jonathan Buchanan <jonathan.buchanan@gmail.com>
|
Jonathan Buchanan <jonathan.buchanan@gmail.com>
|
||||||
|
Jacob Burch <jacobburch@gmail.com>
|
||||||
Keith Bussell <kbussell@gmail.com>
|
Keith Bussell <kbussell@gmail.com>
|
||||||
C8E
|
C8E
|
||||||
Chris Cahoon <chris.cahoon@gmail.com>
|
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>
|
Paul Collins <paul.collins.iii@gmail.com>
|
||||||
Robert Coup
|
Robert Coup
|
||||||
Deric Crago <deric.crago@gmail.com>
|
Deric Crago <deric.crago@gmail.com>
|
||||||
|
Brian Fabian Crain <http://www.bfc.do/>
|
||||||
David Cramer <dcramer@gmail.com>
|
David Cramer <dcramer@gmail.com>
|
||||||
Pete Crosier <pete.crosier@gmail.com>
|
Pete Crosier <pete.crosier@gmail.com>
|
||||||
Matt Croydon <http://www.postneo.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>
|
Leah Culver <leah.culver@gmail.com>
|
||||||
Raúl Cumplido <raulcumplido@gmail.com>
|
Raúl Cumplido <raulcumplido@gmail.com>
|
||||||
flavio.curella@gmail.com
|
flavio.curella@gmail.com
|
||||||
|
Tome Cvitan <tome@cvitan.com>
|
||||||
John D'Agostino <john.dagostino@gmail.com>
|
John D'Agostino <john.dagostino@gmail.com>
|
||||||
dackze+django@gmail.com
|
dackze+django@gmail.com
|
||||||
Jim Dalton <jim.dalton@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>
|
J. Clifford Dyer <jcd@sdf.lonestar.org>
|
||||||
Clint Ecker
|
Clint Ecker
|
||||||
Nick Efford <nick@efford.org>
|
Nick Efford <nick@efford.org>
|
||||||
|
Marc Egli <frog32@me.com>
|
||||||
eibaan@gmail.com
|
eibaan@gmail.com
|
||||||
David Eklund
|
David Eklund
|
||||||
Julia Elman
|
Julia Elman
|
||||||
@ -212,6 +225,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Stefane Fermgier <sf@fermigier.com>
|
Stefane Fermgier <sf@fermigier.com>
|
||||||
J. Pablo Fernandez <pupeno@pupeno.com>
|
J. Pablo Fernandez <pupeno@pupeno.com>
|
||||||
Maciej Fijalkowski
|
Maciej Fijalkowski
|
||||||
|
Leandra Finger <leandra.finger@gmail.com>
|
||||||
Juan Pedro Fisanotti <fisadev@gmail.com>
|
Juan Pedro Fisanotti <fisadev@gmail.com>
|
||||||
Ben Firshman <ben@firshman.co.uk>
|
Ben Firshman <ben@firshman.co.uk>
|
||||||
Matthew Flanagan <http://wadofstuff.blogspot.com>
|
Matthew Flanagan <http://wadofstuff.blogspot.com>
|
||||||
@ -239,6 +253,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
pradeep.gowda@gmail.com
|
pradeep.gowda@gmail.com
|
||||||
Collin Grady <collin@collingrady.com>
|
Collin Grady <collin@collingrady.com>
|
||||||
Gabriel Grant <g@briel.ca>
|
Gabriel Grant <g@briel.ca>
|
||||||
|
Martin Green
|
||||||
Daniel Greenfeld
|
Daniel Greenfeld
|
||||||
Simon Greenhill <dev@simon.net.nz>
|
Simon Greenhill <dev@simon.net.nz>
|
||||||
Owen Griffiths
|
Owen Griffiths
|
||||||
@ -268,6 +283,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Eric Holscher <http://ericholscher.com>
|
Eric Holscher <http://ericholscher.com>
|
||||||
Ian Holsman <http://feh.holsman.net/>
|
Ian Holsman <http://feh.holsman.net/>
|
||||||
Kieran Holland <http://www.kieranholland.com>
|
Kieran Holland <http://www.kieranholland.com>
|
||||||
|
Markus Holtermann <http://markusholtermann.eu>
|
||||||
Sung-Jin Hong <serialx.net@gmail.com>
|
Sung-Jin Hong <serialx.net@gmail.com>
|
||||||
Leo "hylje" Honkanen <sealage@gmail.com>
|
Leo "hylje" Honkanen <sealage@gmail.com>
|
||||||
Matt Hoskins <skaffenuk@googlemail.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/>
|
Rob Hudson <http://rob.cogit8.org/>
|
||||||
Jason Huggins <http://www.jrandolph.com/blog/>
|
Jason Huggins <http://www.jrandolph.com/blog/>
|
||||||
Jeff Hui <jeffkhui@gmail.com>
|
Jeff Hui <jeffkhui@gmail.com>
|
||||||
Gabriel Hurley <gabriel@strikeawe.com>
|
|
||||||
Hyun Mi Ae
|
Hyun Mi Ae
|
||||||
Ibon <ibonso@gmail.com>
|
Ibon <ibonso@gmail.com>
|
||||||
Tom Insam
|
Tom Insam
|
||||||
@ -327,12 +342,12 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Meir Kriheli <http://mksoft.co.il/>
|
Meir Kriheli <http://mksoft.co.il/>
|
||||||
Bruce Kroeze <http://coderseye.com/>
|
Bruce Kroeze <http://coderseye.com/>
|
||||||
krzysiek.pawlik@silvermedia.pl
|
krzysiek.pawlik@silvermedia.pl
|
||||||
Joseph Kocherhans
|
|
||||||
konrad@gwu.edu
|
konrad@gwu.edu
|
||||||
knox <christobzr@gmail.com>
|
knox <christobzr@gmail.com>
|
||||||
David Krauth
|
David Krauth
|
||||||
Kevin Kubasik <kevin@kubasik.net>
|
Kevin Kubasik <kevin@kubasik.net>
|
||||||
kurtiss@meetro.com
|
kurtiss@meetro.com
|
||||||
|
Vladimir Kuzma <vladimirkuzma.ch@gmail.com>
|
||||||
Denis Kuzmichyov <kuzmichyov@gmail.com>
|
Denis Kuzmichyov <kuzmichyov@gmail.com>
|
||||||
Panos Laganakos <panos.laganakos@gmail.com>
|
Panos Laganakos <panos.laganakos@gmail.com>
|
||||||
Nick Lane <nick.lane.au@gmail.com>
|
Nick Lane <nick.lane.au@gmail.com>
|
||||||
@ -360,7 +375,6 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
limodou
|
limodou
|
||||||
Philip Lindborg <philip.lindborg@gmail.com>
|
Philip Lindborg <philip.lindborg@gmail.com>
|
||||||
Simon Litchfield <simon@quo.com.au>
|
Simon Litchfield <simon@quo.com.au>
|
||||||
Daniel Lindsley <daniel@toastdriven.com>
|
|
||||||
Trey Long <trey@ktrl.com>
|
Trey Long <trey@ktrl.com>
|
||||||
Laurent Luce <http://www.laurentluce.com>
|
Laurent Luce <http://www.laurentluce.com>
|
||||||
Martin Mahner <http://www.mahner.org/>
|
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>
|
Slawek Mikula <slawek dot mikula at gmail dot com>
|
||||||
Katie Miller <katie@sub50.com>
|
Katie Miller <katie@sub50.com>
|
||||||
Shawn Milochik <shawn@milochik.com>
|
Shawn Milochik <shawn@milochik.com>
|
||||||
|
Baptiste Mispelon <bmispelon@gmail.com>
|
||||||
mitakummaa@gmail.com
|
mitakummaa@gmail.com
|
||||||
Taylor Mitchell <taylor.mitchell@gmail.com>
|
Taylor Mitchell <taylor.mitchell@gmail.com>
|
||||||
mmarshall
|
mmarshall
|
||||||
@ -458,6 +473,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
|
Jyrki Pulliainen <jyrki.pulliainen@gmail.com>
|
||||||
Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
|
Thejaswi Puthraya <thejaswi.puthraya@gmail.com>
|
||||||
Johann Queuniet <johann.queuniet@adh.naellia.eu>
|
Johann Queuniet <johann.queuniet@adh.naellia.eu>
|
||||||
|
Ram Rachum <ram@rachum.com>
|
||||||
Jan Rademaker
|
Jan Rademaker
|
||||||
Michael Radziej <mir@noris.de>
|
Michael Radziej <mir@noris.de>
|
||||||
Laurent Rahuel <laurent.rahuel@gmail.com>
|
Laurent Rahuel <laurent.rahuel@gmail.com>
|
||||||
@ -465,6 +481,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Luciano Ramalho
|
Luciano Ramalho
|
||||||
Amit Ramon <amit.ramon@gmail.com>
|
Amit Ramon <amit.ramon@gmail.com>
|
||||||
Philippe Raoult <philippe.raoult@n2nsoft.com>
|
Philippe Raoult <philippe.raoult@n2nsoft.com>
|
||||||
|
Senko Rašić <senko.rasic@dobarkod.hr>
|
||||||
Massimiliano Ravelli <massimiliano.ravelli@gmail.com>
|
Massimiliano Ravelli <massimiliano.ravelli@gmail.com>
|
||||||
Brian Ray <http://brianray.chipy.org/>
|
Brian Ray <http://brianray.chipy.org/>
|
||||||
Lee Reilly <lee@leereilly.net>
|
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>
|
Alex Robbins <alexander.j.robbins@gmail.com>
|
||||||
Matt Robenolt <m@robenolt.com>
|
Matt Robenolt <m@robenolt.com>
|
||||||
Henrique Romano <onaiort@gmail.com>
|
Henrique Romano <onaiort@gmail.com>
|
||||||
|
Erik Romijn <django@solidlinks.nl>
|
||||||
Armin Ronacher
|
Armin Ronacher
|
||||||
Daniel Roseman <http://roseman.org.uk/>
|
Daniel Roseman <http://roseman.org.uk/>
|
||||||
Rozza <ross.lawley@gmail.com>
|
Rozza <ross.lawley@gmail.com>
|
||||||
@ -499,6 +517,7 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Bernd Schlapsi
|
Bernd Schlapsi
|
||||||
schwank@gmail.com
|
schwank@gmail.com
|
||||||
scott@staplefish.com
|
scott@staplefish.com
|
||||||
|
Olivier Sels <olivier.sels@gmail.com>
|
||||||
Ilya Semenov <semenov@inetss.com>
|
Ilya Semenov <semenov@inetss.com>
|
||||||
Aleksandra Sendecka <asendecka@hauru.eu>
|
Aleksandra Sendecka <asendecka@hauru.eu>
|
||||||
serbaut@gmail.com
|
serbaut@gmail.com
|
||||||
@ -523,11 +542,13 @@ answer newbie questions, and generally made Django that much better:
|
|||||||
Don Spaulding <donspauldingii@gmail.com>
|
Don Spaulding <donspauldingii@gmail.com>
|
||||||
Calvin Spealman <ironfroggy@gmail.com>
|
Calvin Spealman <ironfroggy@gmail.com>
|
||||||
Dane Springmeyer
|
Dane Springmeyer
|
||||||
|
Silvan Spross <silvan.spross@gmail.com>
|
||||||
Bjørn Stabell <bjorn@exoweb.net>
|
Bjørn Stabell <bjorn@exoweb.net>
|
||||||
Georgi Stanojevski <glisha@gmail.com>
|
Georgi Stanojevski <glisha@gmail.com>
|
||||||
starrynight <cmorgh@gmail.com>
|
starrynight <cmorgh@gmail.com>
|
||||||
Vasiliy Stavenko <stavenko@gmail.com>
|
Vasiliy Stavenko <stavenko@gmail.com>
|
||||||
Thomas Steinacher <http://www.eggdrop.ch/>
|
Thomas Steinacher <http://www.eggdrop.ch/>
|
||||||
|
Emil Stenström <em@kth.se>
|
||||||
Johan C. Stöver <johan@nilling.nl>
|
Johan C. Stöver <johan@nilling.nl>
|
||||||
Nowell Strite <http://nowell.strite.org/>
|
Nowell Strite <http://nowell.strite.org/>
|
||||||
Thomas Stromberg <tstromberg@google.com>
|
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>
|
I.S. van Oostveen <v.oostveen@idca.nl>
|
||||||
viestards.lists@gmail.com
|
viestards.lists@gmail.com
|
||||||
George Vilches <gav@thataddress.com>
|
George Vilches <gav@thataddress.com>
|
||||||
|
Simeon Visser <http://simeonvisser.com>
|
||||||
Vlado <vlado@labath.org>
|
Vlado <vlado@labath.org>
|
||||||
Zachary Voase <zacharyvoase@gmail.com>
|
Zachary Voase <zacharyvoase@gmail.com>
|
||||||
Marijn Vriens <marijn@metronomo.cl>
|
Marijn Vriens <marijn@metronomo.cl>
|
||||||
Milton Waddams
|
Milton Waddams
|
||||||
Chris Wagner <cw264701@ohio.edu>
|
Chris Wagner <cw264701@ohio.edu>
|
||||||
Rick Wagner <rwagner@physics.ucsd.edu>
|
Rick Wagner <rwagner@physics.ucsd.edu>
|
||||||
|
Gavin Wahl <gavinwahl@gmail.com>
|
||||||
wam-djangobug@wamber.net
|
wam-djangobug@wamber.net
|
||||||
Wang Chun <wangchun@exoweb.net>
|
Wang Chun <wangchun@exoweb.net>
|
||||||
Filip Wasilewski <filip.wasilewski@gmail.com>
|
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):
|
def get_version(*args, **kwargs):
|
||||||
# Don't litter django/__init__.py with all the get_version stuff.
|
# Don't litter django/__init__.py with all the get_version stuff.
|
||||||
|
@ -127,7 +127,10 @@ class Settings(BaseSettings):
|
|||||||
try:
|
try:
|
||||||
mod = importlib.import_module(self.SETTINGS_MODULE)
|
mod = importlib.import_module(self.SETTINGS_MODULE)
|
||||||
except ImportError as e:
|
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
|
# Settings that should be converted into tuples if they're mistakenly entered
|
||||||
# as strings.
|
# as strings.
|
||||||
|
@ -131,7 +131,7 @@ LANGUAGES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Languages using BiDi (right-to-left) layout
|
# 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
|
# If you set this to False, Django will make some optimizations so as not
|
||||||
# to load the internationalization machinery.
|
# to load the internationalization machinery.
|
||||||
|
@ -4,7 +4,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django\n"
|
"Project-Id-Version: Django\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||||
"Last-Translator: Django team\n"
|
"Last-Translator: Django team\n"
|
||||||
"Language-Team: English <en@li.org>\n"
|
"Language-Team: English <en@li.org>\n"
|
||||||
@ -337,7 +337,7 @@ msgstr ""
|
|||||||
msgid "Enter a valid value."
|
msgid "Enter a valid value."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: core/validators.py:53 forms/fields.py:640
|
#: core/validators.py:53 forms/fields.py:639
|
||||||
msgid "Enter a valid URL."
|
msgid "Enter a valid URL."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -362,7 +362,7 @@ msgstr ""
|
|||||||
msgid "Enter a valid IPv4 or IPv6 address."
|
msgid "Enter a valid IPv4 or IPv6 address."
|
||||||
msgstr ""
|
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."
|
msgid "Enter only digits separated by commas."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -408,7 +408,7 @@ msgstr[1] ""
|
|||||||
msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s."
|
msgid "%(field_name)s must be unique for %(date_field)s %(lookup)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/base.py:905 forms/models.py:605
|
#: db/models/base.py:905 forms/models.py:643
|
||||||
msgid "and"
|
msgid "and"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -435,156 +435,156 @@ msgstr ""
|
|||||||
msgid "Field of type: %(field_type)s"
|
msgid "Field of type: %(field_type)s"
|
||||||
msgstr ""
|
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"
|
msgid "Integer"
|
||||||
msgstr ""
|
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
|
#, python-format
|
||||||
msgid "'%s' value must be an integer."
|
msgid "'%s' value must be an integer."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:620
|
#: db/models/fields/__init__.py:622
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' value must be either True or False."
|
msgid "'%s' value must be either True or False."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:622
|
#: db/models/fields/__init__.py:624
|
||||||
msgid "Boolean (Either True or False)"
|
msgid "Boolean (Either True or False)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:671
|
#: db/models/fields/__init__.py:673
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "String (up to %(max_length)s)"
|
msgid "String (up to %(max_length)s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:699
|
#: db/models/fields/__init__.py:701
|
||||||
msgid "Comma-separated integers"
|
msgid "Comma-separated integers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:713
|
#: db/models/fields/__init__.py:715
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format."
|
msgid "'%s' value has an invalid date format. It must be in YYYY-MM-DD format."
|
||||||
msgstr ""
|
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
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date."
|
"'%s' value has the correct format (YYYY-MM-DD) but it is an invalid date."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:718
|
#: db/models/fields/__init__.py:720
|
||||||
msgid "Date (without time)"
|
msgid "Date (without time)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:801
|
#: db/models/fields/__init__.py:803
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[."
|
"'%s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[."
|
||||||
"uuuuuu]][TZ] format."
|
"uuuuuu]][TZ] format."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:805
|
#: db/models/fields/__init__.py:807
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but "
|
"'%s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]) but "
|
||||||
"it is an invalid date/time."
|
"it is an invalid date/time."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:809
|
#: db/models/fields/__init__.py:811
|
||||||
msgid "Date (with time)"
|
msgid "Date (with time)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:898
|
#: db/models/fields/__init__.py:900
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' value must be a decimal number."
|
msgid "'%s' value must be a decimal number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:900
|
#: db/models/fields/__init__.py:902
|
||||||
msgid "Decimal number"
|
msgid "Decimal number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:957
|
#: db/models/fields/__init__.py:959
|
||||||
msgid "Email address"
|
msgid "Email address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:976
|
#: db/models/fields/__init__.py:978
|
||||||
msgid "File path"
|
msgid "File path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1003
|
#: db/models/fields/__init__.py:1005
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' value must be a float."
|
msgid "'%s' value must be a float."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1005
|
#: db/models/fields/__init__.py:1007
|
||||||
msgid "Floating point number"
|
msgid "Floating point number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1066
|
#: db/models/fields/__init__.py:1068
|
||||||
msgid "Big (8 byte) integer"
|
msgid "Big (8 byte) integer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1080
|
#: db/models/fields/__init__.py:1082
|
||||||
msgid "IPv4 address"
|
msgid "IPv4 address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1096
|
#: db/models/fields/__init__.py:1098
|
||||||
msgid "IP address"
|
msgid "IP address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1139
|
#: db/models/fields/__init__.py:1141
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "'%s' value must be either None, True or False."
|
msgid "'%s' value must be either None, True or False."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1141
|
#: db/models/fields/__init__.py:1143
|
||||||
msgid "Boolean (Either True, False or None)"
|
msgid "Boolean (Either True, False or None)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1190
|
#: db/models/fields/__init__.py:1192
|
||||||
msgid "Positive integer"
|
msgid "Positive integer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1201
|
#: db/models/fields/__init__.py:1203
|
||||||
msgid "Positive small integer"
|
msgid "Positive small integer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1212
|
#: db/models/fields/__init__.py:1214
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Slug (up to %(max_length)s)"
|
msgid "Slug (up to %(max_length)s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1230
|
#: db/models/fields/__init__.py:1232
|
||||||
msgid "Small integer"
|
msgid "Small integer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1236
|
#: db/models/fields/__init__.py:1238
|
||||||
msgid "Text"
|
msgid "Text"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1254
|
#: db/models/fields/__init__.py:1256
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format."
|
"'%s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] format."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1256
|
#: db/models/fields/__init__.py:1258
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid "
|
"'%s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an invalid "
|
||||||
"time."
|
"time."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1259
|
#: db/models/fields/__init__.py:1261
|
||||||
msgid "Time"
|
msgid "Time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1321
|
#: db/models/fields/__init__.py:1323
|
||||||
msgid "URL"
|
msgid "URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/__init__.py:1338
|
#: db/models/fields/__init__.py:1340
|
||||||
msgid "Raw binary data"
|
msgid "Raw binary data"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -596,55 +596,50 @@ msgstr ""
|
|||||||
msgid "Image"
|
msgid "Image"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/related.py:1133
|
#: db/models/fields/related.py:1118
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Model %(model)s with pk %(pk)r does not exist."
|
msgid "Model %(model)s with pk %(pk)r does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/related.py:1135
|
#: db/models/fields/related.py:1120
|
||||||
msgid "Foreign Key (type determined by related field)"
|
msgid "Foreign Key (type determined by related field)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/related.py:1272
|
#: db/models/fields/related.py:1257
|
||||||
msgid "One-to-one relationship"
|
msgid "One-to-one relationship"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: db/models/fields/related.py:1339
|
#: db/models/fields/related.py:1324
|
||||||
msgid "Many-to-many relationship"
|
msgid "Many-to-many relationship"
|
||||||
msgstr ""
|
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
|
#: forms/fields.py:56
|
||||||
msgid "This field is required."
|
msgid "This field is required."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:225
|
#: forms/fields.py:227
|
||||||
msgid "Enter a whole number."
|
msgid "Enter a whole number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:266 forms/fields.py:294
|
#: forms/fields.py:268 forms/fields.py:296
|
||||||
msgid "Enter a number."
|
msgid "Enter a number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:296
|
#: forms/fields.py:298
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ensure that there are no more than %(max)s digit in total."
|
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."
|
msgid_plural "Ensure that there are no more than %(max)s digits in total."
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: forms/fields.py:300
|
#: forms/fields.py:302
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ensure that there are no more than %(max)s decimal place."
|
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."
|
msgid_plural "Ensure that there are no more than %(max)s decimal places."
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: forms/fields.py:304
|
#: forms/fields.py:306
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Ensure that there are no more than %(max)s digit before the decimal point."
|
"Ensure that there are no more than %(max)s digit before the decimal point."
|
||||||
@ -653,31 +648,31 @@ msgid_plural ""
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: forms/fields.py:406 forms/fields.py:1058
|
#: forms/fields.py:408 forms/fields.py:1064
|
||||||
msgid "Enter a valid date."
|
msgid "Enter a valid date."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:430 forms/fields.py:1059
|
#: forms/fields.py:432 forms/fields.py:1065
|
||||||
msgid "Enter a valid time."
|
msgid "Enter a valid time."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:451
|
#: forms/fields.py:454
|
||||||
msgid "Enter a valid date/time."
|
msgid "Enter a valid date/time."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:525
|
#: forms/fields.py:531
|
||||||
msgid "No file was submitted. Check the encoding type on the form."
|
msgid "No file was submitted. Check the encoding type on the form."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:526
|
#: forms/fields.py:532
|
||||||
msgid "No file was submitted."
|
msgid "No file was submitted."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:527
|
#: forms/fields.py:533
|
||||||
msgid "The submitted file is empty."
|
msgid "The submitted file is empty."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:529
|
#: forms/fields.py:535
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Ensure this filename has at most %(max)d character (it has %(length)d)."
|
msgid "Ensure this filename has at most %(max)d character (it has %(length)d)."
|
||||||
msgid_plural ""
|
msgid_plural ""
|
||||||
@ -685,22 +680,22 @@ msgid_plural ""
|
|||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: forms/fields.py:532
|
#: forms/fields.py:538
|
||||||
msgid "Please either submit a file or check the clear checkbox, not both."
|
msgid "Please either submit a file or check the clear checkbox, not both."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/fields.py:593
|
#: forms/fields.py:599
|
||||||
msgid ""
|
msgid ""
|
||||||
"Upload a valid image. The file you uploaded was either not an image or a "
|
"Upload a valid image. The file you uploaded was either not an image or a "
|
||||||
"corrupted image."
|
"corrupted image."
|
||||||
msgstr ""
|
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
|
#, python-format
|
||||||
msgid "Select a valid choice. %(value)s is not one of the available choices."
|
msgid "Select a valid choice. %(value)s is not one of the available choices."
|
||||||
msgstr ""
|
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."
|
msgid "Enter a list of values."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -709,53 +704,60 @@ msgstr ""
|
|||||||
msgid "(Hidden field %(name)s) %(error)s"
|
msgid "(Hidden field %(name)s) %(error)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/formsets.py:305
|
#: forms/formsets.py:310
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Please submit %s or fewer forms."
|
msgid "Please submit %d or fewer forms."
|
||||||
msgstr ""
|
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"
|
msgid "Order"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/formsets.py:335
|
#: forms/formsets.py:341
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/models.py:599
|
#: forms/models.py:637
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Please correct the duplicate data for %(field)s."
|
msgid "Please correct the duplicate data for %(field)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/models.py:603
|
#: forms/models.py:641
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Please correct the duplicate data for %(field)s, which must be unique."
|
msgid "Please correct the duplicate data for %(field)s, which must be unique."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/models.py:609
|
#: forms/models.py:647
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Please correct the duplicate data for %(field_name)s which must be unique "
|
"Please correct the duplicate data for %(field_name)s which must be unique "
|
||||||
"for the %(lookup)s in %(date_field)s."
|
"for the %(lookup)s in %(date_field)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/models.py:617
|
#: forms/models.py:655
|
||||||
msgid "Please correct the duplicate values below."
|
msgid "Please correct the duplicate values below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/models.py:883
|
#: forms/models.py:937
|
||||||
msgid "The inline foreign key did not match the parent instance primary key."
|
msgid "The inline foreign key did not match the parent instance primary key."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/models.py:947
|
#: forms/models.py:1001
|
||||||
msgid "Select a valid choice. That choice is not one of the available choices."
|
msgid "Select a valid choice. That choice is not one of the available choices."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: forms/models.py:1044
|
#: forms/models.py:1098
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "\"%(pk)s\" is not a valid value for a primary key."
|
msgid "\"%(pk)s\" is not a valid value for a primary key."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: forms/models.py:1109
|
||||||
|
msgid ""
|
||||||
|
"Hold down \"Control\", or \"Command\" on a Mac, to select more than one."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: forms/util.py:84
|
#: forms/util.py:84
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -791,34 +793,34 @@ msgstr ""
|
|||||||
msgid "yes,no,maybe"
|
msgid "yes,no,maybe"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: template/defaultfilters.py:813 template/defaultfilters.py:824
|
#: template/defaultfilters.py:813 template/defaultfilters.py:825
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(size)d byte"
|
msgid "%(size)d byte"
|
||||||
msgid_plural "%(size)d bytes"
|
msgid_plural "%(size)d bytes"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: template/defaultfilters.py:826
|
#: template/defaultfilters.py:827
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s KB"
|
msgid "%s KB"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: template/defaultfilters.py:828
|
#: template/defaultfilters.py:829
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s MB"
|
msgid "%s MB"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: template/defaultfilters.py:830
|
#: template/defaultfilters.py:831
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s GB"
|
msgid "%s GB"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: template/defaultfilters.py:832
|
#: template/defaultfilters.py:833
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s TB"
|
msgid "%s TB"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: template/defaultfilters.py:833
|
#: template/defaultfilters.py:835
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s PB"
|
msgid "%s PB"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -1119,6 +1121,16 @@ msgctxt "alt. month"
|
|||||||
msgid "December"
|
msgid "December"
|
||||||
msgstr ""
|
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
|
#: utils/text.py:70
|
||||||
#, python-format
|
#, python-format
|
||||||
msgctxt "String to return when truncating text"
|
msgctxt "String to return when truncating text"
|
||||||
@ -1130,53 +1142,53 @@ msgid "or"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Translators: This string is used as a separator between list elements
|
#. 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 ", "
|
msgid ", "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: utils/timesince.py:22
|
#: utils/timesince.py:23
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%d year"
|
msgid "%d year"
|
||||||
msgid_plural "%d years"
|
msgid_plural "%d years"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: utils/timesince.py:23
|
#: utils/timesince.py:24
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%d month"
|
msgid "%d month"
|
||||||
msgid_plural "%d months"
|
msgid_plural "%d months"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: utils/timesince.py:24
|
#: utils/timesince.py:25
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%d week"
|
msgid "%d week"
|
||||||
msgid_plural "%d weeks"
|
msgid_plural "%d weeks"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: utils/timesince.py:25
|
#: utils/timesince.py:26
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%d day"
|
msgid "%d day"
|
||||||
msgid_plural "%d days"
|
msgid_plural "%d days"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: utils/timesince.py:26
|
#: utils/timesince.py:27
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%d hour"
|
msgid "%d hour"
|
||||||
msgid_plural "%d hours"
|
msgid_plural "%d hours"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: utils/timesince.py:27
|
#: utils/timesince.py:28
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%d minute"
|
msgid "%d minute"
|
||||||
msgid_plural "%d minutes"
|
msgid_plural "%d minutes"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: utils/timesince.py:43
|
#: utils/timesince.py:44
|
||||||
msgid "0 minutes"
|
msgid "0 minutes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -5,8 +5,9 @@ from django.utils.importlib import import_module
|
|||||||
from django.utils import six
|
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'
|
handler403 = 'django.views.defaults.permission_denied'
|
||||||
handler404 = 'django.views.defaults.page_not_found'
|
handler404 = 'django.views.defaults.page_not_found'
|
||||||
handler500 = 'django.views.defaults.server_error'
|
handler500 = 'django.views.defaults.server_error'
|
||||||
|
6
django/contrib/admin/exceptions.py
Normal file
6
django/contrib/admin/exceptions.py
Normal file
@ -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: (
|
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)
|
isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
|
||||||
|
|
||||||
|
|
||||||
|
@ -131,7 +131,7 @@ class AdminField(object):
|
|||||||
classes.append('required')
|
classes.append('required')
|
||||||
if not self.is_first:
|
if not self.is_first:
|
||||||
classes.append('inline')
|
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)
|
return self.field.label_tag(contents=mark_safe(contents), attrs=attrs)
|
||||||
|
|
||||||
def errors(self):
|
def errors(self):
|
||||||
@ -144,7 +144,7 @@ class AdminReadonlyField(object):
|
|||||||
# {{ field.name }} must be a useful class name to identify the field.
|
# {{ field.name }} must be a useful class name to identify the field.
|
||||||
# For convenience, store other field-related data here too.
|
# For convenience, store other field-related data here too.
|
||||||
if callable(field):
|
if callable(field):
|
||||||
class_name = field.__name__ != '<lambda>' and field.__name__ or ''
|
class_name = field.__name__ if field.__name__ != '<lambda>' else ''
|
||||||
else:
|
else:
|
||||||
class_name = field
|
class_name = field
|
||||||
self.field = {
|
self.field = {
|
||||||
|
@ -4,7 +4,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django\n"
|
"Project-Id-Version: Django\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||||
"Last-Translator: Django team\n"
|
"Last-Translator: Django team\n"
|
||||||
"Language-Team: English <en@li.org>\n"
|
"Language-Team: English <en@li.org>\n"
|
||||||
@ -18,12 +18,12 @@ msgstr ""
|
|||||||
msgid "Successfully deleted %(count)d %(items)s."
|
msgid "Successfully deleted %(count)d %(items)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:61 options.py:1365
|
#: actions.py:61 options.py:1418
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Cannot delete %(name)s"
|
msgid "Cannot delete %(name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: actions.py:63 options.py:1367
|
#: actions.py:63 options.py:1420
|
||||||
msgid "Are you sure?"
|
msgid "Are you sure?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -130,157 +130,157 @@ msgstr ""
|
|||||||
msgid "LogEntry Object"
|
msgid "LogEntry Object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:163 options.py:192
|
#: options.py:173 options.py:202
|
||||||
msgid "None"
|
msgid "None"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:710
|
#: options.py:763
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Changed %s."
|
msgid "Changed %s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:710 options.py:720 options.py:1514
|
#: options.py:763 options.py:773 options.py:1570
|
||||||
msgid "and"
|
msgid "and"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:715
|
#: options.py:768
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Added %(name)s \"%(object)s\"."
|
msgid "Added %(name)s \"%(object)s\"."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:719
|
#: options.py:772
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Changed %(list)s for %(name)s \"%(object)s\"."
|
msgid "Changed %(list)s for %(name)s \"%(object)s\"."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:724
|
#: options.py:777
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Deleted %(name)s \"%(object)s\"."
|
msgid "Deleted %(name)s \"%(object)s\"."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:728
|
#: options.py:781
|
||||||
msgid "No fields changed."
|
msgid "No fields changed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:831 options.py:874
|
#: options.py:884 options.py:927
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The %(name)s \"%(obj)s\" was added successfully. You may edit it again below."
|
"The %(name)s \"%(obj)s\" was added successfully. You may edit it again below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:849
|
#: options.py:902
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The %(name)s \"%(obj)s\" was added successfully. You may add another "
|
"The %(name)s \"%(obj)s\" was added successfully. You may add another "
|
||||||
"%(name)s below."
|
"%(name)s below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:853
|
#: options.py:906
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The %(name)s \"%(obj)s\" was added successfully."
|
msgid "The %(name)s \"%(obj)s\" was added successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:867
|
#: options.py:920
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The %(name)s \"%(obj)s\" was changed successfully. You may edit it again "
|
"The %(name)s \"%(obj)s\" was changed successfully. You may edit it again "
|
||||||
"below."
|
"below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:881
|
#: options.py:934
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The %(name)s \"%(obj)s\" was changed successfully. You may add another "
|
"The %(name)s \"%(obj)s\" was changed successfully. You may add another "
|
||||||
"%(name)s below."
|
"%(name)s below."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:887
|
#: options.py:940
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The %(name)s \"%(obj)s\" was changed successfully."
|
msgid "The %(name)s \"%(obj)s\" was changed successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:965 options.py:1225
|
#: options.py:1018 options.py:1278
|
||||||
msgid ""
|
msgid ""
|
||||||
"Items must be selected in order to perform actions on them. No items have "
|
"Items must be selected in order to perform actions on them. No items have "
|
||||||
"been changed."
|
"been changed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:984
|
#: options.py:1037
|
||||||
msgid "No action selected."
|
msgid "No action selected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1064
|
#: options.py:1117
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Add %s"
|
msgid "Add %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1088 options.py:1333
|
#: options.py:1141 options.py:1386
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(name)s object with primary key %(key)r does not exist."
|
msgid "%(name)s object with primary key %(key)r does not exist."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1154
|
#: options.py:1207
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Change %s"
|
msgid "Change %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1204
|
#: options.py:1257
|
||||||
msgid "Database error"
|
msgid "Database error"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1267
|
#: options.py:1320
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(count)s %(name)s was changed successfully."
|
msgid "%(count)s %(name)s was changed successfully."
|
||||||
msgid_plural "%(count)s %(name)s were changed successfully."
|
msgid_plural "%(count)s %(name)s were changed successfully."
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: options.py:1294
|
#: options.py:1347
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(total_count)s selected"
|
msgid "%(total_count)s selected"
|
||||||
msgid_plural "All %(total_count)s selected"
|
msgid_plural "All %(total_count)s selected"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: options.py:1299
|
#: options.py:1352
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "0 of %(cnt)s selected"
|
msgid "0 of %(cnt)s selected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1350
|
#: options.py:1403
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "The %(name)s \"%(obj)s\" was deleted successfully."
|
msgid "The %(name)s \"%(obj)s\" was deleted successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1406
|
#: options.py:1459
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Change history: %s"
|
msgid "Change history: %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. Translators: Model verbose name and instance representation, suitable to be an item in a list
|
#. Translators: Model verbose name and instance representation, suitable to be an item in a list
|
||||||
#: options.py:1508
|
#: options.py:1564
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%(class_name)s %(instance)s"
|
msgid "%(class_name)s %(instance)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: options.py:1515
|
#: options.py:1571
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Deleting %(class_name)s %(instance)s would require deleting the following "
|
"Deleting %(class_name)s %(instance)s would require deleting the following "
|
||||||
"protected related objects: %(related_objects)s"
|
"protected related objects: %(related_objects)s"
|
||||||
msgstr ""
|
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
|
#: templates/registration/password_reset_complete.html:19
|
||||||
#: views/decorators.py:24
|
#: views/decorators.py:24
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: sites.py:392
|
#: sites.py:386
|
||||||
msgid "Site administration"
|
msgid "Site administration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: sites.py:446
|
#: sites.py:440
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s administration"
|
msgid "%s administration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -429,9 +429,14 @@ msgstr ""
|
|||||||
#: templates/admin/auth/user/change_password.html:27
|
#: templates/admin/auth/user/change_password.html:27
|
||||||
#: templates/registration/password_change_form.html:20
|
#: templates/registration/password_change_form.html:20
|
||||||
msgid "Please correct the error below."
|
msgid "Please correct the error below."
|
||||||
msgid_plural "Please correct the errors below."
|
msgstr ""
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
#: 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
|
#: templates/admin/change_list.html:58
|
||||||
#, python-format
|
#, python-format
|
||||||
@ -811,16 +816,16 @@ msgstr ""
|
|||||||
msgid "All dates"
|
msgid "All dates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/main.py:37
|
#: views/main.py:35
|
||||||
msgid "(None)"
|
msgid "(None)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/main.py:86
|
#: views/main.py:84
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Select %s"
|
msgid "Select %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views/main.py:88
|
#: views/main.py:86
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Select %s to change"
|
msgid "Select %s to change"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import copy
|
import copy
|
||||||
from functools import update_wrapper, partial
|
import operator
|
||||||
|
from functools import partial, reduce, update_wrapper
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
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.contenttypes.models import ContentType
|
||||||
from django.contrib.admin import widgets, helpers
|
from django.contrib.admin import widgets, helpers
|
||||||
from django.contrib.admin.util import (unquote, flatten_fieldsets, get_deleted_objects,
|
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.admin.templatetags.admin_static import static
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.views.decorators.csrf import csrf_protect
|
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.fields import BLANK_CHOICE_DASH, FieldDoesNotExist
|
||||||
from django.db.models.sql.constants import QUERY_TERMS
|
from django.db.models.sql.constants import QUERY_TERMS
|
||||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||||
|
from django.http.response import HttpResponseBase
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.response import SimpleTemplateResponse, TemplateResponse
|
from django.template.response import SimpleTemplateResponse, TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
@ -87,6 +90,14 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
|
|||||||
readonly_fields = ()
|
readonly_fields = ()
|
||||||
ordering = None
|
ordering = None
|
||||||
|
|
||||||
|
# validation
|
||||||
|
validator_class = validation.BaseValidator
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(cls, model):
|
||||||
|
validator = cls.validator_class()
|
||||||
|
validator.validate(cls, model)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
|
overrides = FORMFIELD_FOR_DBFIELD_DEFAULTS.copy()
|
||||||
overrides.update(self.formfield_overrides)
|
overrides.update(self.formfield_overrides)
|
||||||
@ -246,6 +257,34 @@ class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)):
|
|||||||
"""
|
"""
|
||||||
return self.prepopulated_fields
|
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):
|
def get_queryset(self, request):
|
||||||
"""
|
"""
|
||||||
Returns a QuerySet of all model instances that can be edited by the
|
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_on_bottom = False
|
||||||
actions_selection_counter = True
|
actions_selection_counter = True
|
||||||
|
|
||||||
|
# validation
|
||||||
|
validator_class = validation.ModelAdminValidator
|
||||||
|
|
||||||
def __init__(self, model, admin_site):
|
def __init__(self, model, admin_site):
|
||||||
self.model = model
|
self.model = model
|
||||||
self.opts = model._meta
|
self.opts = model._meta
|
||||||
@ -456,7 +498,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
"Hook for specifying fieldsets for the add form."
|
"Hook for specifying fieldsets for the add form."
|
||||||
if self.declared_fieldsets:
|
if self.declared_fieldsets:
|
||||||
return 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))
|
fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj))
|
||||||
return [(None, {'fields': fields})]
|
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
|
Returns a Form class for use in the admin add view. This is used by
|
||||||
add_view and change_view.
|
add_view and change_view.
|
||||||
"""
|
"""
|
||||||
if self.declared_fieldsets:
|
if 'fields' in kwargs:
|
||||||
fields = flatten_fieldsets(self.declared_fieldsets)
|
fields = kwargs.pop('fields')
|
||||||
else:
|
else:
|
||||||
fields = None
|
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
|
||||||
if self.exclude is None:
|
if self.exclude is None:
|
||||||
exclude = []
|
exclude = []
|
||||||
else:
|
else:
|
||||||
@ -985,10 +1027,10 @@ class ModelAdmin(BaseModelAdmin):
|
|||||||
|
|
||||||
response = func(self, request, queryset)
|
response = func(self, request, queryset)
|
||||||
|
|
||||||
# Actions may return an HttpResponse, which will be used as the
|
# Actions may return an HttpResponse-like object, which will be
|
||||||
# response from the POST. If not, we'll be a good little HTTP
|
# used as the response from the POST. If not, we'll be a good
|
||||||
# citizen and redirect back to the changelist page.
|
# little HTTP citizen and redirect back to the changelist page.
|
||||||
if isinstance(response, HttpResponse):
|
if isinstance(response, HttpResponseBase):
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
return HttpResponseRedirect(request.get_full_path())
|
return HttpResponseRedirect(request.get_full_path())
|
||||||
@ -1447,6 +1489,9 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||||||
verbose_name_plural = None
|
verbose_name_plural = None
|
||||||
can_delete = True
|
can_delete = True
|
||||||
|
|
||||||
|
# validation
|
||||||
|
validator_class = validation.InlineValidator
|
||||||
|
|
||||||
def __init__(self, parent_model, admin_site):
|
def __init__(self, parent_model, admin_site):
|
||||||
self.admin_site = admin_site
|
self.admin_site = admin_site
|
||||||
self.parent_model = parent_model
|
self.parent_model = parent_model
|
||||||
@ -1467,12 +1512,20 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||||||
js.extend(['SelectBox.js', 'SelectFilter2.js'])
|
js.extend(['SelectBox.js', 'SelectFilter2.js'])
|
||||||
return forms.Media(js=[static('admin/js/%s' % url) for url in 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):
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
"""Returns a BaseInlineFormSet class for use in admin add/change views."""
|
"""Returns a BaseInlineFormSet class for use in admin add/change views."""
|
||||||
if self.declared_fieldsets:
|
if 'fields' in kwargs:
|
||||||
fields = flatten_fieldsets(self.declared_fieldsets)
|
fields = kwargs.pop('fields')
|
||||||
else:
|
else:
|
||||||
fields = None
|
fields = flatten_fieldsets(self.get_fieldsets(request, obj))
|
||||||
if self.exclude is None:
|
if self.exclude is None:
|
||||||
exclude = []
|
exclude = []
|
||||||
else:
|
else:
|
||||||
@ -1493,8 +1546,8 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||||||
"fields": fields,
|
"fields": fields,
|
||||||
"exclude": exclude,
|
"exclude": exclude,
|
||||||
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
|
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
|
||||||
"extra": self.extra,
|
"extra": self.get_extra(request, obj, **kwargs),
|
||||||
"max_num": self.max_num,
|
"max_num": self.get_max_num(request, obj, **kwargs),
|
||||||
"can_delete": can_delete,
|
"can_delete": can_delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1544,7 +1597,7 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||||||
def get_fieldsets(self, request, obj=None):
|
def get_fieldsets(self, request, obj=None):
|
||||||
if self.declared_fieldsets:
|
if self.declared_fieldsets:
|
||||||
return 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))
|
fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj))
|
||||||
return [(None, {'fields': fields})]
|
return [(None, {'fields': fields})]
|
||||||
|
|
||||||
|
@ -66,12 +66,6 @@ class AdminSite(object):
|
|||||||
if not admin_class:
|
if not admin_class:
|
||||||
admin_class = ModelAdmin
|
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):
|
if isinstance(model_or_iterable, ModelBase):
|
||||||
model_or_iterable = [model_or_iterable]
|
model_or_iterable = [model_or_iterable]
|
||||||
for model in model_or_iterable:
|
for model in model_or_iterable:
|
||||||
@ -94,8 +88,8 @@ class AdminSite(object):
|
|||||||
options['__module__'] = __name__
|
options['__module__'] = __name__
|
||||||
admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
|
admin_class = type("%sAdmin" % model.__name__, (admin_class,), options)
|
||||||
|
|
||||||
# Validate (which might be a no-op)
|
if admin_class is not ModelAdmin and settings.DEBUG:
|
||||||
validate(admin_class, model)
|
admin_class.validate(model)
|
||||||
|
|
||||||
# Instantiate the admin class to save in the registry
|
# Instantiate the admin class to save in the registry
|
||||||
self._registry[model] = admin_class(model, self)
|
self._registry[model] = admin_class(model, self)
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
|
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<p class="errornote">
|
<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>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
|
{% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %}
|
||||||
{% if errors %}
|
{% if errors %}
|
||||||
<p class="errornote">
|
<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>
|
</p>
|
||||||
{{ adminform.form.non_field_errors }}
|
{{ adminform.form.non_field_errors }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% if cl.formset.errors %}
|
{% if cl.formset.errors %}
|
||||||
<p class="errornote">
|
<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>
|
</p>
|
||||||
{{ cl.formset.non_form_errors }}
|
{{ cl.formset.non_form_errors }}
|
||||||
{% endif %}
|
{% 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 %}">
|
<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 %}
|
{% if line.fields|length_is:'1' %}{{ line.errors }}{% endif %}
|
||||||
{% for field in line %}
|
{% 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 not line.fields|length_is:'1' and not field.is_readonly %}{{ field.errors }}{% endif %}
|
||||||
{% if field.is_checkbox %}
|
{% if field.is_checkbox %}
|
||||||
{{ field.field }}{{ field.label_tag }}
|
{{ field.field }}{{ field.label_tag }}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %}
|
{% if form.errors and not form.non_field_errors and not form.this_is_the_login_form.errors %}
|
||||||
<p class="errornote">
|
<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>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<div>
|
<div>
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<p class="errornote">
|
<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>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
{% trans "Please go to the following page and choose a new password:" %}
|
{% trans "Please go to the following page and choose a new password:" %}
|
||||||
{% block reset_link %}
|
{% 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 %}
|
{% endblock %}
|
||||||
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
|
{% 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.
|
# ON_EACH_SIDE links at either end of the "current page" link.
|
||||||
page_range = []
|
page_range = []
|
||||||
if page_num > (ON_EACH_SIDE + ON_ENDS):
|
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.append(DOT)
|
||||||
page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1))
|
page_range.extend(range(page_num - ON_EACH_SIDE, page_num + 1))
|
||||||
else:
|
else:
|
||||||
|
@ -53,4 +53,4 @@ def get_admin_log(parser, token):
|
|||||||
if tokens[4] != 'for_user':
|
if tokens[4] != 'for_user':
|
||||||
raise template.TemplateSyntaxError(
|
raise template.TemplateSyntaxError(
|
||||||
"Fourth argument to 'get_admin_log' must be 'for_user'")
|
"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 ends with __in, split parameter into separate values
|
||||||
if key.endswith('__in'):
|
if key.endswith('__in'):
|
||||||
value = value.split(',')
|
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 key.endswith('__isnull'):
|
||||||
if value.lower() in ('', 'false'):
|
if value.lower() in ('', 'false', '0'):
|
||||||
value = False
|
value = False
|
||||||
else:
|
else:
|
||||||
value = True
|
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):
|
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
|
Returns a sensible label for a field name. The name can be a callable,
|
||||||
name of an object attributes, as well as a genuine fields. If return_attr is
|
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.
|
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.
|
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"):
|
if hasattr(attr, "short_description"):
|
||||||
label = 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):
|
elif callable(attr):
|
||||||
if attr.__name__ == "<lambda>":
|
if attr.__name__ == "<lambda>":
|
||||||
label = "--"
|
label = "--"
|
||||||
@ -315,6 +320,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
|
|||||||
else:
|
else:
|
||||||
return label
|
return label
|
||||||
|
|
||||||
|
|
||||||
def help_text_for_field(name, model):
|
def help_text_for_field(name, model):
|
||||||
try:
|
try:
|
||||||
help_text = model._meta.get_field_by_name(name)[0].help_text
|
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.db.models.fields import FieldDoesNotExist
|
||||||
from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
|
from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
|
||||||
_get_foreign_key)
|
_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.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):
|
def validate(self, cls, model):
|
||||||
"""
|
for m in dir(self):
|
||||||
Does basic ModelAdmin option validation. Calls custom validation
|
if m.startswith('validate_'):
|
||||||
classmethod in the end if it is provided in cls. The signature of the
|
getattr(self, m)(cls, model)
|
||||||
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()
|
|
||||||
|
|
||||||
opts = model._meta
|
def check_field_spec(self, cls, model, flds, label):
|
||||||
validate_base(cls, model)
|
"""
|
||||||
|
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
|
The fields specification can be a ``fields`` option or a ``fields``
|
||||||
if hasattr(cls, 'list_display'):
|
sub-option from a ``fieldsets`` option component.
|
||||||
check_isseq(cls, 'list_display', cls.list_display)
|
"""
|
||||||
for idx, field in enumerate(cls.list_display):
|
for fields in flds:
|
||||||
if not callable(field):
|
# The entry in fields might be a tuple. If it is a standalone
|
||||||
if not hasattr(cls, field):
|
# field, make it into a tuple to make processing easier.
|
||||||
if not hasattr(model, field):
|
if type(fields) != tuple:
|
||||||
try:
|
fields = (fields,)
|
||||||
opts.get_field(field)
|
for field in fields:
|
||||||
except models.FieldDoesNotExist:
|
if field in cls.readonly_fields:
|
||||||
raise ImproperlyConfigured("%s.list_display[%d], %r is not a callable or an attribute of %r or found in the model %r."
|
# Stuff can be put in fields that isn't actually a
|
||||||
% (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
|
# model field if it's in readonly_fields,
|
||||||
else:
|
# readonly_fields will handle the validation of such
|
||||||
# getattr(model, field) could be an X_RelatedObjectsDescriptor
|
# things.
|
||||||
f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field)
|
continue
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
get_fields_from_path(model, field)
|
f = model._meta.get_field(field)
|
||||||
except (NotRelationField, FieldDoesNotExist):
|
except models.FieldDoesNotExist:
|
||||||
raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
|
# If we can't find a field on the model that matches, it could be an
|
||||||
" which does not refer to a Field."
|
# 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))
|
% (cls.__name__, idx, field))
|
||||||
|
|
||||||
# list_per_page = 100
|
def validate_fields(self, cls, model):
|
||||||
if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
|
" Validate that fields only refer to existing fields, doesn't contain duplicates. "
|
||||||
raise ImproperlyConfigured("'%s.list_per_page' should be a integer."
|
# fields
|
||||||
% cls.__name__)
|
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
|
def validate_fieldsets(self, cls, model):
|
||||||
if hasattr(cls, 'list_max_show_all') and not isinstance(cls.list_max_show_all, int):
|
" Validate that fieldsets is properly formatted and doesn't contain duplicates. "
|
||||||
raise ImproperlyConfigured("'%s.list_max_show_all' should be an integer."
|
from django.contrib.admin.options import flatten_fieldsets
|
||||||
% cls.__name__)
|
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
|
def validate_exclude(self, cls, model):
|
||||||
if hasattr(cls, 'list_editable') and cls.list_editable:
|
" Validate that exclude is a sequence without duplicates. "
|
||||||
check_isseq(cls, 'list_editable', cls.list_editable)
|
if cls.exclude: # default value is None
|
||||||
for idx, field_name in enumerate(cls.list_editable):
|
check_isseq(cls, 'exclude', cls.exclude)
|
||||||
try:
|
if len(cls.exclude) > len(set(cls.exclude)):
|
||||||
field = opts.get_field_by_name(field_name)[0]
|
raise ImproperlyConfigured('There are duplicate field(s) in %s.exclude' % cls.__name__)
|
||||||
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))
|
|
||||||
|
|
||||||
# search_fields = ()
|
def validate_form(self, cls, model):
|
||||||
if hasattr(cls, 'search_fields'):
|
" Validate that form subclasses BaseModelForm. "
|
||||||
check_isseq(cls, 'search_fields', cls.search_fields)
|
if hasattr(cls, 'form') and not issubclass(cls.form, BaseModelForm):
|
||||||
|
raise ImproperlyConfigured("%s.form does not inherit from "
|
||||||
|
"BaseModelForm." % cls.__name__)
|
||||||
|
|
||||||
# date_hierarchy = None
|
def validate_filter_vertical(self, cls, model):
|
||||||
if cls.date_hierarchy:
|
" Validate that filter_vertical is a sequence of field names. "
|
||||||
f = get_field(cls, model, opts, 'date_hierarchy', cls.date_hierarchy)
|
if hasattr(cls, 'filter_vertical'):
|
||||||
if not isinstance(f, (models.DateField, models.DateTimeField)):
|
check_isseq(cls, 'filter_vertical', cls.filter_vertical)
|
||||||
raise ImproperlyConfigured("'%s.date_hierarchy is "
|
for idx, field in enumerate(cls.filter_vertical):
|
||||||
"neither an instance of DateField nor DateTimeField."
|
f = get_field(cls, model, 'filter_vertical', field)
|
||||||
% cls.__name__)
|
if not isinstance(f, models.ManyToManyField):
|
||||||
|
raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be "
|
||||||
|
"a ManyToManyField." % (cls.__name__, idx))
|
||||||
|
|
||||||
# ordering = None
|
def validate_filter_horizontal(self, cls, model):
|
||||||
if cls.ordering:
|
" Validate that filter_horizontal is a sequence of field names. "
|
||||||
check_isseq(cls, 'ordering', cls.ordering)
|
if hasattr(cls, 'filter_horizontal'):
|
||||||
for idx, field in enumerate(cls.ordering):
|
check_isseq(cls, 'filter_horizontal', cls.filter_horizontal)
|
||||||
if field == '?' and len(cls.ordering) != 1:
|
for idx, field in enumerate(cls.filter_horizontal):
|
||||||
raise ImproperlyConfigured("'%s.ordering' has the random "
|
f = get_field(cls, model, 'filter_horizontal', field)
|
||||||
"ordering marker '?', but contains other fields as "
|
if not isinstance(f, models.ManyToManyField):
|
||||||
"well. Please either remove '?' or the other fields."
|
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__)
|
% 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 = []
|
class InlineValidator(BaseValidator):
|
||||||
if hasattr(cls, 'inlines'):
|
def validate_fk_name(self, cls, model):
|
||||||
check_isseq(cls, 'inlines', cls.inlines)
|
" Validate that fk_name refers to a ForeignKey. "
|
||||||
for idx, inline in enumerate(cls.inlines):
|
if cls.fk_name: # default value is None
|
||||||
if not issubclass(inline, BaseModelAdmin):
|
f = get_field(cls, model, 'fk_name', cls.fk_name)
|
||||||
raise ImproperlyConfigured("'%s.inlines[%d]' does not inherit "
|
if not isinstance(f, models.ForeignKey):
|
||||||
"from BaseModelAdmin." % (cls.__name__, idx))
|
raise ImproperlyConfigured("'%s.fk_name is not an instance of "
|
||||||
if not inline.model:
|
"models.ForeignKey." % cls.__name__)
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
def validate_max_num(self, cls, model):
|
||||||
if cls.fk_name: # default value is None
|
" Validate that max_num is an integer. "
|
||||||
f = get_field(cls, cls.model, cls.model._meta, 'fk_name', cls.fk_name)
|
check_type(cls, 'max_num', int)
|
||||||
if not isinstance(f, models.ForeignKey):
|
|
||||||
raise ImproperlyConfigured("'%s.fk_name is not an instance of "
|
|
||||||
"models.ForeignKey." % cls.__name__)
|
|
||||||
|
|
||||||
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
|
def check_type(cls, attr, type_):
|
||||||
max_num = getattr(cls, 'max_num', None)
|
if getattr(cls, attr, None) is not None and not isinstance(getattr(cls, attr), type_):
|
||||||
if max_num is not None and not isinstance(max_num, int):
|
raise ImproperlyConfigured("'%s.%s' should be a %s."
|
||||||
raise ImproperlyConfigured("'%s.max_num' should be an integer or None (default)."
|
% (cls.__name__, attr, type_.__name__ ))
|
||||||
% 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_isseq(cls, label, obj):
|
def check_isseq(cls, label, obj):
|
||||||
if not isinstance(obj, (list, tuple)):
|
if not isinstance(obj, (list, tuple)):
|
||||||
@ -364,16 +411,16 @@ def check_isdict(cls, label, obj):
|
|||||||
if not isinstance(obj, dict):
|
if not isinstance(obj, dict):
|
||||||
raise ImproperlyConfigured("'%s.%s' must be a dictionary." % (cls.__name__, label))
|
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:
|
try:
|
||||||
return opts.get_field(field)
|
return model._meta.get_field(field)
|
||||||
except models.FieldDoesNotExist:
|
except models.FieldDoesNotExist:
|
||||||
raise ImproperlyConfigured("'%s.%s' refers to field '%s' that is missing from model '%s.%s'."
|
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__))
|
% (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:
|
try:
|
||||||
return opts.get_field(field)
|
return model._meta.get_field(field)
|
||||||
except models.FieldDoesNotExist:
|
except models.FieldDoesNotExist:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
@ -381,15 +428,3 @@ def fetch_attr(cls, model, opts, label, field):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ImproperlyConfigured("'%s.%s' refers to '%s' that is neither a field, method or property of model '%s.%s'."
|
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__))
|
% (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 sys
|
||||||
import warnings
|
import warnings
|
||||||
from functools import reduce
|
|
||||||
|
|
||||||
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
|
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
|
||||||
from django.core.paginator import InvalidPage
|
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.utils.http import urlencode
|
||||||
|
|
||||||
from django.contrib.admin import FieldListFilter
|
from django.contrib.admin import FieldListFilter
|
||||||
|
from django.contrib.admin.exceptions import DisallowedModelAdminLookup
|
||||||
from django.contrib.admin.options import IncorrectLookupParameters
|
from django.contrib.admin.options import IncorrectLookupParameters
|
||||||
from django.contrib.admin.util import (quote, get_fields_from_path,
|
from django.contrib.admin.util import (quote, get_fields_from_path,
|
||||||
lookup_needs_distinct, prepare_lookup_value)
|
lookup_needs_distinct, prepare_lookup_value)
|
||||||
@ -130,7 +129,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
|
|||||||
lookup_params[force_str(key)] = value
|
lookup_params[force_str(key)] = value
|
||||||
|
|
||||||
if not self.model_admin.lookup_allowed(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 = []
|
filter_specs = []
|
||||||
if self.list_filter:
|
if self.list_filter:
|
||||||
@ -331,7 +330,7 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
|
|||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
# First, we collect all the declared list filters.
|
# First, we collect all the declared list filters.
|
||||||
(self.filter_specs, self.has_filters, remaining_lookup_params,
|
(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.
|
# Then, we let every list filter modify the queryset to its liking.
|
||||||
qs = self.root_queryset
|
qs = self.root_queryset
|
||||||
@ -357,56 +356,46 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
|
|||||||
# ValueError, ValidationError, or ?.
|
# ValueError, ValidationError, or ?.
|
||||||
raise IncorrectLookupParameters(e)
|
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 not qs.query.select_related:
|
||||||
if self.list_select_related:
|
qs = self.apply_select_related(qs)
|
||||||
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
|
|
||||||
|
|
||||||
# Set ordering.
|
# Set ordering.
|
||||||
ordering = self.get_ordering(request, qs)
|
ordering = self.get_ordering(request, qs)
|
||||||
qs = qs.order_by(*ordering)
|
qs = qs.order_by(*ordering)
|
||||||
|
|
||||||
# Apply keyword searches.
|
# Apply search results
|
||||||
def construct_search(field_name):
|
qs, search_use_distinct = self.model_admin.get_search_results(
|
||||||
if field_name.startswith('^'):
|
request, qs, self.query)
|
||||||
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
|
|
||||||
|
|
||||||
if self.search_fields and self.query:
|
# Remove duplicates from results, if necessary
|
||||||
orm_lookups = [construct_search(str(search_field))
|
if filters_use_distinct | search_use_distinct:
|
||||||
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:
|
|
||||||
return qs.distinct()
|
return qs.distinct()
|
||||||
else:
|
else:
|
||||||
return qs
|
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):
|
def url_for_result(self, result):
|
||||||
pk = getattr(result, self.pk_attname)
|
pk = getattr(result, self.pk_attname)
|
||||||
return reverse('admin:%s_%s_change' % (self.opts.app_label,
|
return reverse('admin:%s_%s_change' % (self.opts.app_label,
|
||||||
|
@ -4,7 +4,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django\n"
|
"Project-Id-Version: Django\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||||
"Last-Translator: Django team\n"
|
"Last-Translator: Django team\n"
|
||||||
"Language-Team: English <en@li.org>\n"
|
"Language-Team: English <en@li.org>\n"
|
||||||
@ -60,12 +60,13 @@ msgstr ""
|
|||||||
msgid "number of %s"
|
msgid "number of %s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views.py:270
|
#. Translators: %s is an object type name
|
||||||
|
#: views.py:271
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Fields on %s objects"
|
msgid "Attributes on %s objects"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: views.py:362
|
#: views.py:363
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "%s does not appear to be a urlpattern object"
|
msgid "%s does not appear to be a urlpattern object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -83,7 +84,9 @@ msgid "Home"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin_doc/bookmarklets.html:7 templates/admin_doc/index.html:7
|
#: 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:7
|
||||||
|
#: templates/admin_doc/missing_docutils.html:14
|
||||||
#: templates/admin_doc/model_detail.html:15
|
#: templates/admin_doc/model_detail.html:15
|
||||||
#: templates/admin_doc/model_index.html:9
|
#: templates/admin_doc/model_index.html:9
|
||||||
#: templates/admin_doc/template_detail.html:7
|
#: templates/admin_doc/template_detail.html:7
|
||||||
@ -94,7 +97,7 @@ msgstr ""
|
|||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin_doc/bookmarklets.html:8
|
#: templates/admin_doc/bookmarklets.html:8 templates/admin_doc/index.html:29
|
||||||
msgid "Bookmarklets"
|
msgid "Bookmarklets"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -149,26 +152,202 @@ msgstr ""
|
|||||||
msgid "As above, but opens the admin page in a new window."
|
msgid "As above, but opens the admin page in a new window."
|
||||||
msgstr ""
|
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:10
|
||||||
|
#: templates/admin_doc/model_index.html:14
|
||||||
msgid "Models"
|
msgid "Models"
|
||||||
msgstr ""
|
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
|
#: templates/admin_doc/template_detail.html:8
|
||||||
msgid "Templates"
|
msgid "Templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin_doc/template_filter_index.html:9
|
#: templates/admin_doc/template_detail.html:13
|
||||||
msgid "Filters"
|
#, python-format
|
||||||
|
msgid "Template: %(name)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin_doc/template_tag_index.html:9
|
#: templates/admin_doc/template_detail.html:16
|
||||||
msgid "Tags"
|
#, python-format
|
||||||
|
msgid "Template: \"%(name)s\""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/admin_doc/view_detail.html:8
|
#: templates/admin_doc/template_detail.html:20
|
||||||
#: templates/admin_doc/view_index.html:9
|
#, python-format
|
||||||
msgid "Views"
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: tests/test_fields.py:29
|
#: tests/test_fields.py:29
|
||||||
|
23
django/contrib/admindocs/middleware.py
Normal file
23
django/contrib/admindocs/middleware.py
Normal file
@ -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>
|
› {% trans 'Documentation' %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}Documentation{% endblock %}
|
{% block title %}{% trans 'Documentation' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>Documentation</h1>
|
<h1>{% trans 'Documentation' %}</h1>
|
||||||
|
|
||||||
<div id="content-main">
|
<div id="content-main">
|
||||||
<h3><a href="tags/">Tags</a></h3>
|
<h3><a href="tags/">{% trans 'Tags' %}</a></h3>
|
||||||
<p>List of all the template tags and their functions.</p>
|
<p>{% trans 'List of all the template tags and their functions.' %}</p>
|
||||||
|
|
||||||
<h3><a href="filters/">Filters</a></h3>
|
<h3><a href="filters/">{% trans 'Filters' %}</a></h3>
|
||||||
<p>Filters are actions which can be applied to variables in a template to alter the output.</p>
|
<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>
|
<h3><a href="models/">{% trans '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>
|
<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>
|
<h3><a href="views/">{% trans '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>
|
<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>
|
<h3><a href="bookmarklets/">{% trans 'Bookmarklets' %}</a></h3>
|
||||||
<p>Tools for your browser to quickly access admin functionality.</p>
|
<p>{% trans 'Tools for your browser to quickly access admin functionality.' %}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,16 +7,16 @@
|
|||||||
› {% trans 'Documentation' %}</a>
|
› {% trans 'Documentation' %}</a>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}Please install docutils{% endblock %}
|
{% block title %}{% trans 'Please install docutils' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>Documentation</h1>
|
<h1>{% trans 'Documentation' %}</h1>
|
||||||
|
|
||||||
<div id="content-main">
|
<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>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}Model: {{ name }}{% endblock %}
|
{% block title %}{% blocktrans %}Model: {{ name }}{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="content-main">
|
<div id="content-main">
|
||||||
@ -32,9 +32,9 @@
|
|||||||
<table class="model">
|
<table class="model">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Field</th>
|
<th>{% trans 'Field' %}</th>
|
||||||
<th>Type</th>
|
<th>{% trans 'Type' %}</th>
|
||||||
<th>Description</th>
|
<th>{% trans 'Description' %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -49,6 +49,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -11,11 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}Models{% endblock %}
|
{% block title %}{% trans 'Models' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>Model documentation</h1>
|
<h1>{% trans 'Model documentation' %}</h1>
|
||||||
|
|
||||||
{% regroup models by app_label as grouped_models %}
|
{% regroup models by app_label as grouped_models %}
|
||||||
|
|
||||||
@ -40,7 +40,7 @@
|
|||||||
{% block sidebar %}
|
{% block sidebar %}
|
||||||
<div id="content-related" class="sidebar">
|
<div id="content-related" class="sidebar">
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<h2>Model groups</h2>
|
<h2>{% trans 'Model groups' %}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% regroup models by app_label as grouped_models %}
|
{% regroup models by app_label as grouped_models %}
|
||||||
{% for group in grouped_models %}
|
{% for group in grouped_models %}
|
||||||
|
@ -10,20 +10,20 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}Template: {{ name }}{% endblock %}
|
{% block title %}{% blocktrans %}Template: {{ name }}{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Template: "{{ name }}"</h1>
|
<h1>{% blocktrans %}Template: "{{ name }}"{% endblocktrans %}</h1>
|
||||||
|
|
||||||
{% regroup templates|dictsort:"site_id" by site as templates_by_site %}
|
{% regroup templates|dictsort:"site_id" by site as templates_by_site %}
|
||||||
{% for group in 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>
|
<ol>
|
||||||
{% for template in group.list|dictsort:"order" %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
@ -9,18 +9,18 @@
|
|||||||
› {% trans 'Filters' %}
|
› {% trans 'Filters' %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}Template filters{% endblock %}
|
{% block title %}{% trans 'Template filters' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>Template filter documentation</h1>
|
<h1>{% trans 'Template filter documentation' %}</h1>
|
||||||
|
|
||||||
<div id="content-main">
|
<div id="content-main">
|
||||||
{% regroup filters|dictsort:"library" by library as filter_libraries %}
|
{% regroup filters|dictsort:"library" by library as filter_libraries %}
|
||||||
{% for library in filter_libraries %}
|
{% for library in filter_libraries %}
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<h2>{% firstof library.grouper "Built-in filters" %}</h2>
|
<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 %}
|
{% 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" %}
|
{% for filter in library.list|dictsort:"name" %}
|
||||||
<h3 id="{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</h3>
|
<h3 id="{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</h3>
|
||||||
{{ filter.title }}
|
{{ filter.title }}
|
||||||
@ -40,7 +40,7 @@
|
|||||||
{% regroup filters|dictsort:"library" by library as filter_libraries %}
|
{% regroup filters|dictsort:"library" by library as filter_libraries %}
|
||||||
{% for library in filter_libraries %}
|
{% for library in filter_libraries %}
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<h2>{% firstof library.grouper "Built-in filters" %}</h2>
|
<h2>{% firstof library.grouper _("Built-in filters") %}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for filter in library.list|dictsort:"name" %}
|
{% for filter in library.list|dictsort:"name" %}
|
||||||
<li><a href="#{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</a></li>
|
<li><a href="#{{ library.grouper|default:"built_in" }}-{{ filter.name }}">{{ filter.name }}</a></li>
|
||||||
|
@ -9,18 +9,18 @@
|
|||||||
› {% trans 'Tags' %}
|
› {% trans 'Tags' %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}Template tags{% endblock %}
|
{% block title %}{% trans 'Template tags' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>Template tag documentation</h1>
|
<h1>{% trans 'Template tag documentation' %}</h1>
|
||||||
|
|
||||||
<div id="content-main">
|
<div id="content-main">
|
||||||
{% regroup tags|dictsort:"library" by library as tag_libraries %}
|
{% regroup tags|dictsort:"library" by library as tag_libraries %}
|
||||||
{% for library in tag_libraries %}
|
{% for library in tag_libraries %}
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<h2>{% firstof library.grouper "Built-in tags" %}</h2>
|
<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 %}
|
{% 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" %}
|
{% for tag in library.list|dictsort:"name" %}
|
||||||
<h3 id="{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</h3>
|
<h3 id="{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</h3>
|
||||||
<h4>{{ tag.title|striptags }}</h4>
|
<h4>{{ tag.title|striptags }}</h4>
|
||||||
@ -40,7 +40,7 @@
|
|||||||
{% regroup tags|dictsort:"library" by library as tag_libraries %}
|
{% regroup tags|dictsort:"library" by library as tag_libraries %}
|
||||||
{% for library in tag_libraries %}
|
{% for library in tag_libraries %}
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<h2>{% firstof library.grouper "Built-in tags" %}</h2>
|
<h2>{% firstof library.grouper _("Built-in tags") %}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for tag in library.list|dictsort:"name" %}
|
{% for tag in library.list|dictsort:"name" %}
|
||||||
<li><a href="#{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</a></li>
|
<li><a href="#{{ library.grouper|default:"built_in" }}-{{ tag.name }}">{{ tag.name }}</a></li>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
› {{ name }}
|
› {{ name }}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}View: {{ name }}{% endblock %}
|
{% block title %}{% blocktrans %}View: {{ name }}{% endblocktrans %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -20,14 +20,14 @@
|
|||||||
{{ body }}
|
{{ body }}
|
||||||
|
|
||||||
{% if meta.Context %}
|
{% if meta.Context %}
|
||||||
<h3>Context:</h3>
|
<h3>{% trans 'Context:' %}</h3>
|
||||||
<p>{{ meta.Context }}</p>
|
<p>{{ meta.Context }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if meta.Templates %}
|
{% if meta.Templates %}
|
||||||
<h3>Templates:</h3>
|
<h3>{% trans 'Templates:' %}</h3>
|
||||||
<p>{{ meta.Templates }}</p>
|
<p>{{ meta.Templates }}</p>
|
||||||
{% endif %}
|
{% 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 %}
|
{% endblock %}
|
||||||
|
@ -9,17 +9,17 @@
|
|||||||
› {% trans 'Views' %}
|
› {% trans 'Views' %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block title %}Views{% endblock %}
|
{% block title %}{% trans 'Views' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<h1>View documentation</h1>
|
<h1>{% trans 'View documentation' %}</h1>
|
||||||
|
|
||||||
{% regroup views|dictsort:"site_id" by site as views_by_site %}
|
{% regroup views|dictsort:"site_id" by site as views_by_site %}
|
||||||
|
|
||||||
<div id="content-related" class="sidebar">
|
<div id="content-related" class="sidebar">
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<h2>Jump to site</h2>
|
<h2>{% trans 'Jump to site' %}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{% for site_views in views_by_site %}
|
{% for site_views in views_by_site %}
|
||||||
<li><a href="#site{{ site_views.grouper.id }}">{{ site_views.grouper.name }}</a></li>
|
<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 %}
|
{% for site_views in views_by_site %}
|
||||||
<div class="module">
|
<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" %}
|
{% for view in site_views.list|dictsort:"url" %}
|
||||||
{% ifchanged %}
|
{% ifchanged %}
|
||||||
<h3><a href="{% url 'django-admindocs-views-detail' view=view.full_name %}">{{ view.url }}</a></h3>
|
<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>
|
<p>{{ view.title }}</p>
|
||||||
<hr />
|
<hr />
|
||||||
{% endifchanged %}
|
{% endifchanged %}
|
||||||
@ -46,5 +46,3 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ def bookmarklets(request):
|
|||||||
admin_root = urlresolvers.reverse('admin:index')
|
admin_root = urlresolvers.reverse('admin:index')
|
||||||
return render_to_response('admin_doc/bookmarklets.html', {
|
return render_to_response('admin_doc/bookmarklets.html', {
|
||||||
'root_path': admin_root,
|
'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))
|
}, context_instance=RequestContext(request))
|
||||||
|
|
||||||
@staff_member_required
|
@staff_member_required
|
||||||
@ -267,6 +267,7 @@ def model_detail(request, app_label, model_name):
|
|||||||
return render_to_response('admin_doc/model_detail.html', {
|
return render_to_response('admin_doc/model_detail.html', {
|
||||||
'root_path': urlresolvers.reverse('admin:index'),
|
'root_path': urlresolvers.reverse('admin:index'),
|
||||||
'name': '%s.%s' % (opts.app_label, opts.object_name),
|
'name': '%s.%s' % (opts.app_label, opts.object_name),
|
||||||
|
# Translators: %s is an object type name
|
||||||
'summary': _("Attributes on %s objects") % opts.object_name,
|
'summary': _("Attributes on %s objects") % opts.object_name,
|
||||||
'description': model.__doc__,
|
'description': model.__doc__,
|
||||||
'fields': fields,
|
'fields': fields,
|
||||||
@ -286,7 +287,7 @@ def template_detail(request, template):
|
|||||||
templates.append({
|
templates.append({
|
||||||
'file': template_file,
|
'file': template_file,
|
||||||
'exists': os.path.exists(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_id': settings_mod.SITE_ID,
|
||||||
'site': site_obj,
|
'site': site_obj,
|
||||||
'order': list(settings_mod.TEMPLATE_DIRS).index(dir),
|
'order': list(settings_mod.TEMPLATE_DIRS).index(dir),
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import re
|
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.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||||
from django.utils.module_loading import import_by_path
|
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'
|
SESSION_KEY = '_auth_user_id'
|
||||||
BACKEND_SESSION_KEY = '_auth_user_backend'
|
BACKEND_SESSION_KEY = '_auth_user_backend'
|
||||||
@ -14,7 +17,6 @@ def load_backend(path):
|
|||||||
|
|
||||||
|
|
||||||
def get_backends():
|
def get_backends():
|
||||||
from django.conf import settings
|
|
||||||
backends = []
|
backends = []
|
||||||
for backend_path in settings.AUTHENTICATION_BACKENDS:
|
for backend_path in settings.AUTHENTICATION_BACKENDS:
|
||||||
backends.append(load_backend(backend_path))
|
backends.append(load_backend(backend_path))
|
||||||
@ -83,6 +85,7 @@ def login(request, user):
|
|||||||
request.session[BACKEND_SESSION_KEY] = user.backend
|
request.session[BACKEND_SESSION_KEY] = user.backend
|
||||||
if hasattr(request, 'user'):
|
if hasattr(request, 'user'):
|
||||||
request.user = user
|
request.user = user
|
||||||
|
rotate_token(request)
|
||||||
user_logged_in.send(sender=user.__class__, request=request, user=user)
|
user_logged_in.send(sender=user.__class__, request=request, user=user)
|
||||||
|
|
||||||
|
|
||||||
@ -106,7 +109,6 @@ def logout(request):
|
|||||||
|
|
||||||
def get_user_model():
|
def get_user_model():
|
||||||
"Return the User model that is active in this project"
|
"Return the User model that is active in this project"
|
||||||
from django.conf import settings
|
|
||||||
from django.db.models import get_model
|
from django.db.models import get_model
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -120,12 +122,13 @@ def get_user_model():
|
|||||||
|
|
||||||
|
|
||||||
def get_user(request):
|
def get_user(request):
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from .models import AnonymousUser
|
||||||
try:
|
try:
|
||||||
user_id = request.session[SESSION_KEY]
|
user_id = request.session[SESSION_KEY]
|
||||||
backend_path = request.session[BACKEND_SESSION_KEY]
|
backend_path = request.session[BACKEND_SESSION_KEY]
|
||||||
|
assert backend_path in settings.AUTHENTICATION_BACKENDS
|
||||||
backend = load_backend(backend_path)
|
backend = load_backend(backend_path)
|
||||||
user = backend.get_user(user_id) or AnonymousUser()
|
user = backend.get_user(user_id) or AnonymousUser()
|
||||||
except KeyError:
|
except (KeyError, AssertionError):
|
||||||
user = AnonymousUser()
|
user = AnonymousUser()
|
||||||
return user
|
return user
|
||||||
|
@ -237,7 +237,7 @@ class PasswordResetForm(forms.Form):
|
|||||||
'uid': int_to_base36(user.pk),
|
'uid': int_to_base36(user.pk),
|
||||||
'user': user,
|
'user': user,
|
||||||
'token': token_generator.make_token(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)
|
subject = loader.render_to_string(subject_template_name, c)
|
||||||
# Email subject *must not* contain newlines
|
# 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):
|
for perm in _get_all_permissions(klass._meta, ctype):
|
||||||
searched_perms.append((ctype, perm))
|
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
|
# looking for. We don't need to check for codenames since we already have
|
||||||
# a list of the ones we're going to create.
|
# a list of the ones we're going to create.
|
||||||
all_perms = set(auth_app.Permission.objects.using(db).filter(
|
all_perms = set(auth_app.Permission.objects.using(db).filter(
|
||||||
|
@ -177,7 +177,7 @@ class UserManager(BaseUserManager):
|
|||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
if not username:
|
if not username:
|
||||||
raise ValueError('The given username must be set')
|
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,
|
user = self.model(username=username, email=email,
|
||||||
is_staff=False, is_active=True, is_superuser=False,
|
is_staff=False, is_active=True, is_superuser=False,
|
||||||
last_login=now, date_joined=now, **extra_fields)
|
last_login=now, date_joined=now, **extra_fields)
|
||||||
|
@ -2,12 +2,14 @@ from __future__ import unicode_literals
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.conf import settings
|
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.models import User, Group, Permission, AnonymousUser
|
||||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||||
from django.contrib.auth.tests.test_custom_user import ExtensionUser, CustomPermissionsUser, CustomUser
|
from django.contrib.auth.tests.test_custom_user import ExtensionUser, CustomPermissionsUser, CustomUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
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 import TestCase
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
@ -402,3 +404,52 @@ class PermissionDeniedBackendTest(TestCase):
|
|||||||
settings.AUTHENTICATION_BACKENDS) + (backend, ))
|
settings.AUTHENTICATION_BACKENDS) + (backend, ))
|
||||||
def test_authenticates(self):
|
def test_authenticates(self):
|
||||||
self.assertEqual(authenticate(username='test', password='test'), self.user1)
|
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')
|
raise ValueError('Users must have an email address')
|
||||||
|
|
||||||
user = self.model(
|
user = self.model(
|
||||||
email=CustomUserManager.normalize_email(email),
|
email=self.normalize_email(email),
|
||||||
date_of_birth=date_of_birth,
|
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.models import User
|
||||||
from django.contrib.auth.tests.test_custom_user import CustomUser
|
from django.contrib.auth.tests.test_custom_user import CustomUser
|
||||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
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 import call_command
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.core.management.validation import get_validation_errors
|
from django.core.management.validation import get_validation_errors
|
||||||
@ -195,6 +196,7 @@ class PermissionDuplicationTestCase(TestCase):
|
|||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
models.Permission._meta.permissions = self._original_permissions
|
models.Permission._meta.permissions = self._original_permissions
|
||||||
|
ContentType.objects.clear_cache()
|
||||||
|
|
||||||
def test_duplicated_permissions(self):
|
def test_duplicated_permissions(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
import re
|
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.conf import global_settings, settings
|
||||||
from django.contrib.sites.models import Site, RequestSite
|
from django.contrib.sites.models import Site, RequestSite
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.exceptions import SuspiciousOperation
|
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
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.encoding import force_text
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.http import urlquote
|
from django.utils.http import urlquote
|
||||||
from django.utils._os import upath
|
from django.utils._os import upath
|
||||||
from django.test import TestCase
|
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 import SESSION_KEY, REDIRECT_FIELD_NAME
|
||||||
from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm,
|
from django.contrib.auth.forms import (AuthenticationForm, PasswordChangeForm,
|
||||||
SetPasswordForm, PasswordResetForm)
|
SetPasswordForm, PasswordResetForm)
|
||||||
from django.contrib.auth.tests.utils import skipIfCustomUser
|
from django.contrib.auth.tests.utils import skipIfCustomUser
|
||||||
|
from django.contrib.auth.views import login as login_view
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@ -46,15 +52,31 @@ class AuthViewsTestCase(TestCase):
|
|||||||
'username': 'testclient',
|
'username': 'testclient',
|
||||||
'password': password,
|
'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)
|
self.assertTrue(SESSION_KEY in self.client.session)
|
||||||
|
return response
|
||||||
|
|
||||||
def assertFormError(self, response, error):
|
def assertFormError(self, response, error):
|
||||||
"""Assert that error is found in response.context['form'] errors"""
|
"""Assert that error is found in response.context['form'] errors"""
|
||||||
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
|
form_errors = list(itertools.chain(*response.context['form'].errors.values()))
|
||||||
self.assertIn(force_text(error), form_errors)
|
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
|
@skipIfCustomUser
|
||||||
class AuthViewNamedURLTests(AuthViewsTestCase):
|
class AuthViewNamedURLTests(AuthViewsTestCase):
|
||||||
@ -132,28 +154,32 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||||||
# produce a meaningful reset URL, we need to be certain that the
|
# 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()
|
# 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.
|
# is invoked, but we check here as a practical consequence.
|
||||||
with self.assertRaises(SuspiciousOperation):
|
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||||
self.client.post('/password_reset/',
|
response = self.client.post('/password_reset/',
|
||||||
{'email': 'staffmember@example.com'},
|
{'email': 'staffmember@example.com'},
|
||||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||||
)
|
)
|
||||||
self.assertEqual(len(mail.outbox), 0)
|
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...)
|
# Skip any 500 handler action (like sending more mail...)
|
||||||
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
@override_settings(DEBUG_PROPAGATE_EXCEPTIONS=True)
|
||||||
def test_poisoned_http_host_admin_site(self):
|
def test_poisoned_http_host_admin_site(self):
|
||||||
"Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
|
"Poisoned HTTP_HOST headers can't be used for reset emails on admin views"
|
||||||
with self.assertRaises(SuspiciousOperation):
|
with patch_logger('django.security.DisallowedHost', 'error') as logger_calls:
|
||||||
self.client.post('/admin_password_reset/',
|
response = self.client.post('/admin_password_reset/',
|
||||||
{'email': 'staffmember@example.com'},
|
{'email': 'staffmember@example.com'},
|
||||||
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
HTTP_HOST='www.example:dr.frankenstein@evil.tld'
|
||||||
)
|
)
|
||||||
self.assertEqual(len(mail.outbox), 0)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
self.assertEqual(len(logger_calls), 1)
|
||||||
|
|
||||||
|
|
||||||
def _test_confirm_start(self):
|
def _test_confirm_start(self):
|
||||||
# Start by creating the email
|
# Start by creating the email
|
||||||
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
return self._read_signup_email(mail.outbox[0])
|
return self._read_signup_email(mail.outbox[0])
|
||||||
|
|
||||||
@ -205,8 +231,6 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||||||
url, path = self._test_confirm_start()
|
url, path = self._test_confirm_start()
|
||||||
response = self.client.post(path, {'new_password1': 'anewpassword',
|
response = self.client.post(path, {'new_password1': 'anewpassword',
|
||||||
'new_password2': 'anewpassword'})
|
'new_password2': 'anewpassword'})
|
||||||
# It redirects us to a 'complete' page:
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
# Check the password has been changed
|
# Check the password has been changed
|
||||||
u = User.objects.get(email='staffmember@example.com')
|
u = User.objects.get(email='staffmember@example.com')
|
||||||
self.assertTrue(u.check_password("anewpassword"))
|
self.assertTrue(u.check_password("anewpassword"))
|
||||||
@ -221,6 +245,47 @@ class PasswordResetTest(AuthViewsTestCase):
|
|||||||
'new_password2': 'x'})
|
'new_password2': 'x'})
|
||||||
self.assertFormError(response, SetPasswordForm.error_messages['password_mismatch'])
|
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')
|
@override_settings(AUTH_USER_MODEL='auth.CustomUser')
|
||||||
class CustomUserPasswordResetTest(AuthViewsTestCase):
|
class CustomUserPasswordResetTest(AuthViewsTestCase):
|
||||||
@ -285,8 +350,6 @@ class ChangePasswordTest(AuthViewsTestCase):
|
|||||||
'new_password1': 'password1',
|
'new_password1': 'password1',
|
||||||
'new_password2': 'password1',
|
'new_password2': 'password1',
|
||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertTrue(response.url.endswith('/password_change/done/'))
|
|
||||||
self.fail_login()
|
self.fail_login()
|
||||||
self.login(password='password1')
|
self.login(password='password1')
|
||||||
|
|
||||||
@ -298,20 +361,50 @@ class ChangePasswordTest(AuthViewsTestCase):
|
|||||||
'new_password2': 'password1',
|
'new_password2': 'password1',
|
||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 302)
|
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):
|
def test_password_change_done_fails(self):
|
||||||
with self.settings(LOGIN_URL='/login/'):
|
response = self.client.get('/password_change/done/')
|
||||||
response = self.client.get('/password_change/done/')
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertURLEqual(response.url, '/login/?next=/password_change/done/')
|
||||||
self.assertTrue(response.url.endswith('/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
|
@skipIfCustomUser
|
||||||
class LoginTest(AuthViewsTestCase):
|
class LoginTest(AuthViewsTestCase):
|
||||||
|
|
||||||
def test_current_site_in_context_after_login(self):
|
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)
|
self.assertEqual(response.status_code, 200)
|
||||||
if Site._meta.installed:
|
if Site._meta.installed:
|
||||||
site = Site.objects.get_current()
|
site = Site.objects.get_current()
|
||||||
@ -323,7 +416,7 @@ class LoginTest(AuthViewsTestCase):
|
|||||||
'Login form is not an AuthenticationForm')
|
'Login form is not an AuthenticationForm')
|
||||||
|
|
||||||
def test_security_check(self, password='password'):
|
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
|
# Those URLs should not pass the security check
|
||||||
for bad_url in ('http://example.com',
|
for bad_url in ('http://example.com',
|
||||||
@ -374,63 +467,103 @@ class LoginTest(AuthViewsTestCase):
|
|||||||
# the custom authentication form used by this login asserts
|
# the custom authentication form used by this login asserts
|
||||||
# that a request is passed to the form successfully.
|
# 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
|
@skipIfCustomUser
|
||||||
class LoginURLSettings(AuthViewsTestCase):
|
class LoginURLSettings(AuthViewsTestCase):
|
||||||
|
"""Tests for settings.LOGIN_URL."""
|
||||||
def setUp(self):
|
def assertLoginURLEquals(self, url, parse_qs=False):
|
||||||
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
|
|
||||||
response = self.client.get('/login_required/')
|
response = self.client.get('/login_required/')
|
||||||
self.assertEqual(response.status_code, 302)
|
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):
|
def test_standard_login_url(self):
|
||||||
login_url = '/login/'
|
self.assertLoginURLEquals('/login/?next=/login_required/')
|
||||||
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('/')))
|
|
||||||
|
|
||||||
|
@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):
|
def test_remote_login_url(self):
|
||||||
login_url = 'http://remote.example.com/login'
|
quoted_next = urlquote('http://testserver/login_required/')
|
||||||
login_required_url = self.get_login_required_url(login_url)
|
expected = 'http://remote.example.com/login?next=%s' % quoted_next
|
||||||
querystring = QueryDict('', mutable=True)
|
self.assertLoginURLEquals(expected)
|
||||||
querystring['next'] = 'http://testserver/login_required/'
|
|
||||||
self.assertEqual(login_required_url,
|
|
||||||
'%s?%s' % (login_url, querystring.urlencode('/')))
|
|
||||||
|
|
||||||
|
@override_settings(LOGIN_URL='https:///login/')
|
||||||
def test_https_login_url(self):
|
def test_https_login_url(self):
|
||||||
login_url = 'https:///login/'
|
quoted_next = urlquote('http://testserver/login_required/')
|
||||||
login_required_url = self.get_login_required_url(login_url)
|
expected = 'https:///login/?next=%s' % quoted_next
|
||||||
querystring = QueryDict('', mutable=True)
|
self.assertLoginURLEquals(expected)
|
||||||
querystring['next'] = 'http://testserver/login_required/'
|
|
||||||
self.assertEqual(login_required_url,
|
|
||||||
'%s?%s' % (login_url, querystring.urlencode('/')))
|
|
||||||
|
|
||||||
|
@override_settings(LOGIN_URL='/login/?pretty=1')
|
||||||
def test_login_url_with_querystring(self):
|
def test_login_url_with_querystring(self):
|
||||||
login_url = '/login/?pretty=1'
|
self.assertLoginURLEquals('/login/?pretty=1&next=/login_required/', parse_qs=True)
|
||||||
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('/'))
|
|
||||||
|
|
||||||
|
@override_settings(LOGIN_URL='http://remote.example.com/login/?next=/default/')
|
||||||
def test_remote_login_url_with_next_querystring(self):
|
def test_remote_login_url_with_next_querystring(self):
|
||||||
login_url = 'http://remote.example.com/login/'
|
quoted_next = urlquote('http://testserver/login_required/')
|
||||||
login_required_url = self.get_login_required_url('%s?next=/default/' %
|
expected = 'http://remote.example.com/login/?next=%s' % quoted_next
|
||||||
login_url)
|
self.assertLoginURLEquals(expected)
|
||||||
querystring = QueryDict('', mutable=True)
|
|
||||||
querystring['next'] = 'http://testserver/login_required/'
|
|
||||||
self.assertEqual(login_required_url, '%s?%s' % (login_url,
|
@skipIfCustomUser
|
||||||
querystring.urlencode('/')))
|
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
|
@skipIfCustomUser
|
||||||
@ -457,11 +590,11 @@ class LogoutTest(AuthViewsTestCase):
|
|||||||
self.login()
|
self.login()
|
||||||
response = self.client.get('/logout/next_page/')
|
response = self.client.get('/logout/next_page/')
|
||||||
self.assertEqual(response.status_code, 302)
|
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/')
|
response = self.client.get('/logout/next_page/?next=/login/')
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertTrue(response.url.endswith('/login/'))
|
self.assertURLEqual(response.url, '/login/')
|
||||||
|
|
||||||
self.confirm_logged_out()
|
self.confirm_logged_out()
|
||||||
|
|
||||||
@ -470,7 +603,7 @@ class LogoutTest(AuthViewsTestCase):
|
|||||||
self.login()
|
self.login()
|
||||||
response = self.client.get('/logout/next_page/')
|
response = self.client.get('/logout/next_page/')
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertTrue(response.url.endswith('/somewhere/'))
|
self.assertURLEqual(response.url, '/somewhere/')
|
||||||
self.confirm_logged_out()
|
self.confirm_logged_out()
|
||||||
|
|
||||||
def test_logout_with_redirect_argument(self):
|
def test_logout_with_redirect_argument(self):
|
||||||
@ -478,7 +611,7 @@ class LogoutTest(AuthViewsTestCase):
|
|||||||
self.login()
|
self.login()
|
||||||
response = self.client.get('/logout/?next=/login/')
|
response = self.client.get('/logout/?next=/login/')
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertTrue(response.url.endswith('/login/'))
|
self.assertURLEqual(response.url, '/login/')
|
||||||
self.confirm_logged_out()
|
self.confirm_logged_out()
|
||||||
|
|
||||||
def test_logout_with_custom_redirect_argument(self):
|
def test_logout_with_custom_redirect_argument(self):
|
||||||
@ -486,11 +619,19 @@ class LogoutTest(AuthViewsTestCase):
|
|||||||
self.login()
|
self.login()
|
||||||
response = self.client.get('/logout/custom_query/?follow=/somewhere/')
|
response = self.client.get('/logout/custom_query/?follow=/somewhere/')
|
||||||
self.assertEqual(response.status_code, 302)
|
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()
|
self.confirm_logged_out()
|
||||||
|
|
||||||
def test_security_check(self, password='password'):
|
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
|
# Those URLs should not pass the security check
|
||||||
for bad_url in ('http://example.com',
|
for bad_url in ('http://example.com',
|
||||||
@ -541,5 +682,7 @@ class ChangelistTests(AuthViewsTestCase):
|
|||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
# A lookup that tries to filter on password isn't OK
|
# 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$')
|
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('',
|
urlpatterns = urlpatterns + patterns('',
|
||||||
(r'^logout/custom_query/$', 'django.contrib.auth.views.logout', dict(redirect_field_name='follow')),
|
(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/$', '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'^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_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'^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_required(password_reset)),
|
||||||
(r'^login_required_login_url/$', login_required(password_reset, login_url='/somewhere/')),
|
(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)
|
auth_logout(request)
|
||||||
|
|
||||||
|
if next_page is not None:
|
||||||
|
next_page = resolve_url(next_page)
|
||||||
|
|
||||||
if redirect_field_name in request.REQUEST:
|
if redirect_field_name in request.REQUEST:
|
||||||
next_page = request.REQUEST[redirect_field_name]
|
next_page = request.REQUEST[redirect_field_name]
|
||||||
# Security check -- don't allow redirection to a different host.
|
# 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,
|
current_app=None,
|
||||||
extra_context=None):
|
extra_context=None):
|
||||||
if post_reset_redirect is 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":
|
if request.method == "POST":
|
||||||
form = password_reset_form(request.POST)
|
form = password_reset_form(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@ -192,7 +197,9 @@ def password_reset_confirm(request, uidb36=None, token=None,
|
|||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
assert uidb36 is not None and token is not None # checked by URLconf
|
assert uidb36 is not None and token is not None # checked by URLconf
|
||||||
if post_reset_redirect is None:
|
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:
|
try:
|
||||||
uid_int = base36_to_int(uidb36)
|
uid_int = base36_to_int(uidb36)
|
||||||
user = UserModel._default_manager.get(pk=uid_int)
|
user = UserModel._default_manager.get(pk=uid_int)
|
||||||
@ -242,7 +249,9 @@ def password_change(request,
|
|||||||
password_change_form=PasswordChangeForm,
|
password_change_form=PasswordChangeForm,
|
||||||
current_app=None, extra_context=None):
|
current_app=None, extra_context=None):
|
||||||
if post_change_redirect is 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":
|
if request.method == "POST":
|
||||||
form = password_change_form(user=request.user, data=request.POST)
|
form = password_change_form(user=request.user, data=request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -4,7 +4,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django\n"
|
"Project-Id-Version: Django\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||||
"Last-Translator: Django team\n"
|
"Last-Translator: Django team\n"
|
||||||
"Language-Team: English <en@li.org>\n"
|
"Language-Team: English <en@li.org>\n"
|
||||||
@ -269,9 +269,11 @@ msgstr ""
|
|||||||
|
|
||||||
#: templates/comments/preview.html:11
|
#: templates/comments/preview.html:11
|
||||||
msgid "Please correct the error below"
|
msgid "Please correct the error below"
|
||||||
msgid_plural "Please correct the errors below"
|
msgstr ""
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
#: templates/comments/preview.html:11
|
||||||
|
msgid "Please correct the errors below"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: templates/comments/preview.html:16
|
#: templates/comments/preview.html:16
|
||||||
msgid "Post your comment"
|
msgid "Post your comment"
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<form action="{% comment_form_target %}" method="post">{% csrf_token %}
|
<form action="{% comment_form_target %}" method="post">{% csrf_token %}
|
||||||
{% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
|
{% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
|
||||||
{% if form.errors %}
|
{% 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 %}
|
{% else %}
|
||||||
<h1>{% trans "Preview your comment" %}</h1>
|
<h1>{% trans "Preview your comment" %}</h1>
|
||||||
<blockquote>{{ comment|linebreaks }}</blockquote>
|
<blockquote>{{ comment|linebreaks }}</blockquote>
|
||||||
|
@ -35,9 +35,10 @@ class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)):
|
|||||||
fields.
|
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.ct_field = ct_field
|
||||||
self.fk_field = fk_field
|
self.fk_field = fk_field
|
||||||
|
self.for_concrete_model = for_concrete_model
|
||||||
|
|
||||||
def contribute_to_class(self, cls, name):
|
def contribute_to_class(self, cls, name):
|
||||||
self.name = 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):
|
def get_content_type(self, obj=None, id=None, using=None):
|
||||||
if obj is not 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:
|
elif id:
|
||||||
return ContentType.objects.db_manager(using).get_for_id(id)
|
return ContentType.objects.db_manager(using).get_for_id(id)
|
||||||
else:
|
else:
|
||||||
@ -160,6 +162,8 @@ class GenericRelation(ForeignObject):
|
|||||||
self.object_id_field_name = kwargs.pop("object_id_field", "object_id")
|
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.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['blank'] = True
|
||||||
kwargs['editable'] = False
|
kwargs['editable'] = False
|
||||||
kwargs['serialize'] = False
|
kwargs['serialize'] = False
|
||||||
@ -201,7 +205,7 @@ class GenericRelation(ForeignObject):
|
|||||||
# Save a reference to which model this class is on for future use
|
# Save a reference to which model this class is on for future use
|
||||||
self.model = cls
|
self.model = cls
|
||||||
# Add the descriptor for the relation
|
# 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):
|
def contribute_to_related_class(self, cls, related):
|
||||||
pass
|
pass
|
||||||
@ -216,7 +220,8 @@ class GenericRelation(ForeignObject):
|
|||||||
"""
|
"""
|
||||||
Returns the content type associated with this field's model.
|
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):
|
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]
|
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(**{
|
return self.rel.to._base_manager.db_manager(using).filter(**{
|
||||||
"%s__pk" % self.content_type_field_name:
|
"%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:
|
"%s__in" % self.object_id_field_name:
|
||||||
[obj.pk for obj in objs]
|
[obj.pk for obj in objs]
|
||||||
})
|
})
|
||||||
@ -247,8 +253,9 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
|||||||
"article.publications", the publications attribute is a
|
"article.publications", the publications attribute is a
|
||||||
ReverseGenericRelatedObjectsDescriptor instance.
|
ReverseGenericRelatedObjectsDescriptor instance.
|
||||||
"""
|
"""
|
||||||
def __init__(self, field):
|
def __init__(self, field, for_concrete_model=True):
|
||||||
self.field = field
|
self.field = field
|
||||||
|
self.for_concrete_model = for_concrete_model
|
||||||
|
|
||||||
def __get__(self, instance, instance_type=None):
|
def __get__(self, instance, instance_type=None):
|
||||||
if instance is None:
|
if instance is None:
|
||||||
@ -261,7 +268,8 @@ class ReverseGenericRelatedObjectsDescriptor(object):
|
|||||||
RelatedManager = create_generic_related_manager(superclass)
|
RelatedManager = create_generic_related_manager(superclass)
|
||||||
|
|
||||||
qn = connection.ops.quote_name
|
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]
|
join_cols = self.field.get_joining_columns(reverse_join=True)[0]
|
||||||
manager = RelatedManager(
|
manager = RelatedManager(
|
||||||
@ -376,7 +384,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, data=None, files=None, instance=None, save_as_new=None,
|
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
|
opts = self.model._meta
|
||||||
self.instance = instance
|
self.instance = instance
|
||||||
self.rel_name = '-'.join((
|
self.rel_name = '-'.join((
|
||||||
@ -389,12 +397,14 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||||||
if queryset is None:
|
if queryset is None:
|
||||||
queryset = self.model._default_manager
|
queryset = self.model._default_manager
|
||||||
qs = queryset.filter(**{
|
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,
|
self.ct_fk_field.name: self.instance.pk,
|
||||||
})
|
})
|
||||||
super(BaseGenericInlineFormSet, self).__init__(
|
super(BaseGenericInlineFormSet, self).__init__(
|
||||||
queryset=qs, data=data, files=files,
|
queryset=qs, data=data, files=files,
|
||||||
prefix=prefix
|
prefix=prefix,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -406,7 +416,8 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
|
|||||||
|
|
||||||
def save_new(self, form, commit=True):
|
def save_new(self, form, commit=True):
|
||||||
kwargs = {
|
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,
|
self.ct_fk_field.get_attname(): self.instance.pk,
|
||||||
}
|
}
|
||||||
new_obj = self.model(**kwargs)
|
new_obj = self.model(**kwargs)
|
||||||
@ -418,7 +429,8 @@ def generic_inlineformset_factory(model, form=ModelForm,
|
|||||||
fields=None, exclude=None,
|
fields=None, exclude=None,
|
||||||
extra=3, can_order=False, can_delete=True,
|
extra=3, can_order=False, can_delete=True,
|
||||||
max_num=None,
|
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.
|
Returns a ``GenericInlineFormSet`` for the given kwargs.
|
||||||
|
|
||||||
@ -444,6 +456,7 @@ def generic_inlineformset_factory(model, form=ModelForm,
|
|||||||
validate_max=validate_max)
|
validate_max=validate_max)
|
||||||
FormSet.ct_field = ct_field
|
FormSet.ct_field = ct_field
|
||||||
FormSet.ct_fk_field = fk_field
|
FormSet.ct_fk_field = fk_field
|
||||||
|
FormSet.for_concrete_model = for_concrete_model
|
||||||
return FormSet
|
return FormSet
|
||||||
|
|
||||||
class GenericInlineModelAdmin(InlineModelAdmin):
|
class GenericInlineModelAdmin(InlineModelAdmin):
|
||||||
|
@ -118,11 +118,13 @@ class ContentTypeManager(models.Manager):
|
|||||||
|
|
||||||
def _add_to_cache(self, using, ct):
|
def _add_to_cache(self, using, ct):
|
||||||
"""Insert a ContentType into the cache."""
|
"""Insert a ContentType into the cache."""
|
||||||
model = ct.model_class()
|
# Note it's possible for ContentType objects to be stale; model_class() will return None.
|
||||||
key = (model._meta.app_label, model._meta.model_name)
|
# 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, {})[key] = ct
|
||||||
self.__class__._cache.setdefault(using, {})[ct.id] = ct
|
self.__class__._cache.setdefault(using, {})[ct.id] = ct
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ContentType(models.Model):
|
class ContentType(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
@ -153,7 +155,6 @@ class ContentType(models.Model):
|
|||||||
|
|
||||||
def model_class(self):
|
def model_class(self):
|
||||||
"Returns the Python model class for this type of content."
|
"Returns the Python model class for this type of content."
|
||||||
from django.db import models
|
|
||||||
return models.get_model(self.app_label, self.model,
|
return models.get_model(self.app_label, self.model,
|
||||||
only_installed=False)
|
only_installed=False)
|
||||||
|
|
||||||
|
@ -274,3 +274,10 @@ class ContentTypesTests(TestCase):
|
|||||||
model = 'OldModel',
|
model = 'OldModel',
|
||||||
)
|
)
|
||||||
self.assertEqual(six.text_type(ct), 'Old model')
|
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.conf import settings
|
||||||
from django.contrib.flatpages.models import FlatPage
|
from django.contrib.flatpages.models import FlatPage
|
||||||
from django.contrib.sites.models import get_current_site
|
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.http import Http404, HttpResponse, HttpResponsePermanentRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template import loader, RequestContext
|
from django.template import loader, RequestContext
|
||||||
@ -70,5 +69,4 @@ def render_flatpage(request, f):
|
|||||||
'flatpage': f,
|
'flatpage': f,
|
||||||
})
|
})
|
||||||
response = HttpResponse(t.render(c))
|
response = HttpResponse(t.render(c))
|
||||||
populate_xheaders(request, response, FlatPage, f.id)
|
|
||||||
return response
|
return response
|
||||||
|
6
django/contrib/formtools/exceptions.py
Normal file
6
django/contrib/formtools/exceptions.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from django.core.exceptions import SuspiciousOperation
|
||||||
|
|
||||||
|
|
||||||
|
class WizardViewCookieModified(SuspiciousOperation):
|
||||||
|
"""Signature of cookie modified"""
|
||||||
|
pass
|
@ -1,8 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.core.exceptions import SuspiciousOperation
|
|
||||||
from django.core.signing import BadSignature
|
from django.core.signing import BadSignature
|
||||||
|
|
||||||
|
from django.contrib.formtools.exceptions import WizardViewCookieModified
|
||||||
from django.contrib.formtools.wizard import storage
|
from django.contrib.formtools.wizard import storage
|
||||||
|
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ class CookieStorage(storage.BaseStorage):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
data = None
|
data = None
|
||||||
except BadSignature:
|
except BadSignature:
|
||||||
raise SuspiciousOperation('WizardView cookie manipulated')
|
raise WizardViewCookieModified('WizardView cookie manipulated')
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
return json.loads(data, cls=json.JSONDecoder)
|
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.backends.postgresql_psycopg2.base import DatabaseOperations
|
||||||
from django.db.utils import DatabaseError
|
from django.db.utils import DatabaseError
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
from .models import GeometryColumns, SpatialRefSys
|
||||||
|
|
||||||
|
|
||||||
#### Classes used in constructing PostGIS spatial SQL ####
|
#### Classes used in constructing PostGIS spatial SQL ####
|
||||||
class PostGISOperator(SpatialOperation):
|
class PostGISOperator(SpatialOperation):
|
||||||
@ -62,6 +66,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||||||
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
|
compiler_module = 'django.contrib.gis.db.models.sql.compiler'
|
||||||
name = 'postgis'
|
name = 'postgis'
|
||||||
postgis = True
|
postgis = True
|
||||||
|
geom_func_prefix = 'ST_'
|
||||||
version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
|
version_regex = re.compile(r'^(?P<major>\d)\.(?P<minor1>\d)\.(?P<minor2>\d+)')
|
||||||
valid_aggregates = dict([(k, None) for k in
|
valid_aggregates = dict([(k, None) for k in
|
||||||
('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
|
('Collect', 'Extent', 'Extent3D', 'MakeLine', 'Union')])
|
||||||
@ -72,45 +77,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
super(PostGISOperations, self).__init__(connection)
|
super(PostGISOperations, self).__init__(connection)
|
||||||
|
|
||||||
# Trying to get the PostGIS version because the function
|
prefix = self.geom_func_prefix
|
||||||
# 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.
|
|
||||||
|
|
||||||
# PostGIS-specific operators. The commented descriptions of these
|
# PostGIS-specific operators. The commented descriptions of these
|
||||||
# operators come from Section 7.6 of the PostGIS 1.4 documentation.
|
# operators come from Section 7.6 of the PostGIS 1.4 documentation.
|
||||||
self.geometry_operators = {
|
self.geometry_operators = {
|
||||||
@ -188,13 +155,13 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||||||
self.geometry_functions.update(self.distance_functions)
|
self.geometry_functions.update(self.distance_functions)
|
||||||
|
|
||||||
# Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
|
# Only PostGIS versions 1.3.4+ have GeoJSON serialization support.
|
||||||
if version < (1, 3, 4):
|
if self.spatial_version < (1, 3, 4):
|
||||||
GEOJSON = False
|
GEOJSON = False
|
||||||
else:
|
else:
|
||||||
GEOJSON = prefix + 'AsGeoJson'
|
GEOJSON = prefix + 'AsGeoJson'
|
||||||
|
|
||||||
# ST_ContainsProperly ST_MakeLine, and ST_GeoHash added in 1.4.
|
# 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'
|
GEOHASH = 'ST_GeoHash'
|
||||||
BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
|
BOUNDINGCIRCLE = 'ST_MinimumBoundingCircle'
|
||||||
self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
|
self.geometry_functions['contains_properly'] = PostGISFunction(prefix, 'ContainsProperly')
|
||||||
@ -202,7 +169,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||||||
GEOHASH, BOUNDINGCIRCLE = False, False
|
GEOHASH, BOUNDINGCIRCLE = False, False
|
||||||
|
|
||||||
# Geography type support added in 1.5.
|
# Geography type support added in 1.5.
|
||||||
if version >= (1, 5, 0):
|
if self.spatial_version >= (1, 5, 0):
|
||||||
self.geography = True
|
self.geography = True
|
||||||
# Only a subset of the operators and functions are available
|
# Only a subset of the operators and functions are available
|
||||||
# for the geography type.
|
# for the geography type.
|
||||||
@ -217,7 +184,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Native geometry type support added in PostGIS 2.0.
|
# Native geometry type support added in PostGIS 2.0.
|
||||||
if version >= (2, 0, 0):
|
if self.spatial_version >= (2, 0, 0):
|
||||||
self.geometry = True
|
self.geometry = True
|
||||||
|
|
||||||
# Creating a dictionary lookup of all GIS terms for PostGIS.
|
# Creating a dictionary lookup of all GIS terms for PostGIS.
|
||||||
@ -260,7 +227,7 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||||||
self.union = prefix + 'Union'
|
self.union = prefix + 'Union'
|
||||||
self.unionagg = prefix + 'Union'
|
self.unionagg = prefix + 'Union'
|
||||||
|
|
||||||
if version >= (2, 0, 0):
|
if self.spatial_version >= (2, 0, 0):
|
||||||
self.extent3d = prefix + '3DExtent'
|
self.extent3d = prefix + '3DExtent'
|
||||||
self.length3d = prefix + '3DLength'
|
self.length3d = prefix + '3DLength'
|
||||||
self.perimeter3d = prefix + '3DPerimeter'
|
self.perimeter3d = prefix + '3DPerimeter'
|
||||||
@ -269,6 +236,30 @@ class PostGISOperations(DatabaseOperations, BaseSpatialOperations):
|
|||||||
self.length3d = prefix + 'Length3D'
|
self.length3d = prefix + 'Length3D'
|
||||||
self.perimeter3d = prefix + 'Perimeter3D'
|
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):
|
def check_aggregate_support(self, aggregate):
|
||||||
"""
|
"""
|
||||||
Checks if the given aggregate name is supported (that is, if it's
|
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.
|
# Routines for getting the OGC-compliant models.
|
||||||
def geometry_columns(self):
|
def geometry_columns(self):
|
||||||
from django.contrib.gis.db.backends.postgis.models import GeometryColumns
|
|
||||||
return GeometryColumns
|
return GeometryColumns
|
||||||
|
|
||||||
def spatial_ref_sys(self):
|
def spatial_ref_sys(self):
|
||||||
from django.contrib.gis.db.backends.postgis.models import SpatialRefSys
|
|
||||||
return SpatialRefSys
|
return SpatialRefSys
|
||||||
|
@ -384,7 +384,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||||||
@property
|
@property
|
||||||
def wkt(self):
|
def wkt(self):
|
||||||
"Returns the WKT (Well-Known Text) representation of this Geometry."
|
"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
|
@property
|
||||||
def hex(self):
|
def hex(self):
|
||||||
@ -395,7 +395,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||||||
"""
|
"""
|
||||||
# A possible faster, all-python, implementation:
|
# A possible faster, all-python, implementation:
|
||||||
# str(self.wkb).encode('hex')
|
# 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
|
@property
|
||||||
def hexewkb(self):
|
def hexewkb(self):
|
||||||
@ -407,7 +407,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||||||
if self.hasz and not GEOS_PREPARE:
|
if self.hasz and not GEOS_PREPARE:
|
||||||
# See: http://trac.osgeo.org/geos/ticket/216
|
# See: http://trac.osgeo.org/geos/ticket/216
|
||||||
raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D HEXEWKB.')
|
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
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
@ -427,7 +427,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||||||
as a Python buffer. SRID and Z values are not included, use the
|
as a Python buffer. SRID and Z values are not included, use the
|
||||||
`ewkb` property instead.
|
`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
|
@property
|
||||||
def ewkb(self):
|
def ewkb(self):
|
||||||
@ -439,7 +439,7 @@ class GEOSGeometry(GEOSBase, ListMixin):
|
|||||||
if self.hasz and not GEOS_PREPARE:
|
if self.hasz and not GEOS_PREPARE:
|
||||||
# See: http://trac.osgeo.org/geos/ticket/216
|
# See: http://trac.osgeo.org/geos/ticket/216
|
||||||
raise GEOSException('Upgrade GEOS to 3.1 to get valid 3D EWKB.')
|
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
|
@property
|
||||||
def kml(self):
|
def kml(self):
|
||||||
|
@ -546,7 +546,7 @@ class LayerMapping(object):
|
|||||||
# Attempting to save.
|
# Attempting to save.
|
||||||
m.save(using=self.using)
|
m.save(using=self.using)
|
||||||
num_saved += 1
|
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:
|
except SystemExit:
|
||||||
raise
|
raise
|
||||||
except Exception as msg:
|
except Exception as msg:
|
||||||
|
@ -4,7 +4,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Django\n"
|
"Project-Id-Version: Django\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
|
||||||
"Last-Translator: Django team\n"
|
"Last-Translator: Django team\n"
|
||||||
"Language-Team: English <en@li.org>\n"
|
"Language-Team: English <en@li.org>\n"
|
||||||
@ -237,54 +237,60 @@ msgctxt "naturaltime"
|
|||||||
msgid "%(delta)s ago"
|
msgid "%(delta)s ago"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templatetags/humanize.py:194 templatetags/humanize.py:216
|
#: templatetags/humanize.py:194 templatetags/humanize.py:219
|
||||||
msgid "now"
|
msgid "now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templatetags/humanize.py:197
|
#. Translators: \\u00a0 is non-breaking space
|
||||||
|
#: templatetags/humanize.py:198
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "a second ago"
|
msgid "a second ago"
|
||||||
msgid_plural "%(count)s seconds ago"
|
msgid_plural "%(count)s\\u00a0seconds ago"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: templatetags/humanize.py:202
|
#. Translators: \\u00a0 is non-breaking space
|
||||||
|
#: templatetags/humanize.py:204
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "a minute ago"
|
msgid "a minute ago"
|
||||||
msgid_plural "%(count)s minutes ago"
|
msgid_plural "%(count)s\\u00a0minutes ago"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: templatetags/humanize.py:207
|
#. Translators: \\u00a0 is non-breaking space
|
||||||
|
#: templatetags/humanize.py:210
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "an hour ago"
|
msgid "an hour ago"
|
||||||
msgid_plural "%(count)s hours ago"
|
msgid_plural "%(count)s\\u00a0hours ago"
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
#: templatetags/humanize.py:213
|
#: templatetags/humanize.py:216
|
||||||
#, python-format
|
#, python-format
|
||||||
msgctxt "naturaltime"
|
msgctxt "naturaltime"
|
||||||
msgid "%(delta)s from now"
|
msgid "%(delta)s from now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templatetags/humanize.py:219
|
#. Translators: \\u00a0 is non-breaking space
|
||||||
|
#: templatetags/humanize.py:223
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "a second from now"
|
msgid "a second from now"
|
||||||
msgid_plural "%(count)s seconds from now"
|
msgid_plural "%(count)s\\u00a0seconds from now"
|
||||||
msgstr[0] ""
|
|
||||||
msgstr[1] ""
|
|
||||||
|
|
||||||
#: templatetags/humanize.py:224
|
|
||||||
#, python-format
|
|
||||||
msgid "a minute from now"
|
|
||||||
msgid_plural "%(count)s minutes from now"
|
|
||||||
msgstr[0] ""
|
msgstr[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#. Translators: \\u00a0 is non-breaking space
|
||||||
#: templatetags/humanize.py:229
|
#: templatetags/humanize.py:229
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "an hour from now"
|
msgid "a minute from now"
|
||||||
msgid_plural "%(count)s hours 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[0] ""
|
||||||
msgstr[1] ""
|
msgstr[1] ""
|
||||||
|
@ -194,17 +194,20 @@ def naturaltime(value):
|
|||||||
return _('now')
|
return _('now')
|
||||||
elif delta.seconds < 60:
|
elif delta.seconds < 60:
|
||||||
return ungettext(
|
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}
|
) % {'count': delta.seconds}
|
||||||
elif delta.seconds // 60 < 60:
|
elif delta.seconds // 60 < 60:
|
||||||
count = delta.seconds // 60
|
count = delta.seconds // 60
|
||||||
return ungettext(
|
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}
|
) % {'count': count}
|
||||||
else:
|
else:
|
||||||
count = delta.seconds // 60 // 60
|
count = delta.seconds // 60 // 60
|
||||||
return ungettext(
|
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}
|
) % {'count': count}
|
||||||
else:
|
else:
|
||||||
delta = value - now
|
delta = value - now
|
||||||
@ -216,15 +219,18 @@ def naturaltime(value):
|
|||||||
return _('now')
|
return _('now')
|
||||||
elif delta.seconds < 60:
|
elif delta.seconds < 60:
|
||||||
return ungettext(
|
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}
|
) % {'count': delta.seconds}
|
||||||
elif delta.seconds // 60 < 60:
|
elif delta.seconds // 60 < 60:
|
||||||
count = delta.seconds // 60
|
count = delta.seconds // 60
|
||||||
return ungettext(
|
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}
|
) % {'count': count}
|
||||||
else:
|
else:
|
||||||
count = delta.seconds // 60 // 60
|
count = delta.seconds // 60 // 60
|
||||||
return ungettext(
|
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}
|
) % {'count': count}
|
||||||
|
@ -19,6 +19,8 @@ from django.utils.translation import ugettext as _
|
|||||||
from django.utils import tzinfo
|
from django.utils import tzinfo
|
||||||
from django.utils.unittest import skipIf
|
from django.utils.unittest import skipIf
|
||||||
|
|
||||||
|
from i18n import TransRealMixin
|
||||||
|
|
||||||
|
|
||||||
# Mock out datetime in some tests so they don't fail occasionally when they
|
# 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
|
# 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)
|
return now.replace(tzinfo=tz) + tz.utcoffset(now)
|
||||||
|
|
||||||
|
|
||||||
class HumanizeTests(TestCase):
|
class HumanizeTests(TransRealMixin, TestCase):
|
||||||
|
|
||||||
def humanize_tester(self, test_list, result_list, method):
|
def humanize_tester(self, test_list, result_list, method):
|
||||||
for test_content, result in zip(test_list, result_list):
|
for test_content, result in zip(test_list, result_list):
|
||||||
@ -195,22 +197,22 @@ class HumanizeTests(TestCase):
|
|||||||
result_list = [
|
result_list = [
|
||||||
'now',
|
'now',
|
||||||
'a second ago',
|
'a second ago',
|
||||||
'30 seconds ago',
|
'30\xa0seconds ago',
|
||||||
'a minute ago',
|
'a minute ago',
|
||||||
'2 minutes ago',
|
'2\xa0minutes ago',
|
||||||
'an hour ago',
|
'an hour ago',
|
||||||
'23 hours ago',
|
'23\xa0hours ago',
|
||||||
'1 day ago',
|
'1\xa0day ago',
|
||||||
'1 year, 4 months ago',
|
'1\xa0year, 4\xa0months ago',
|
||||||
'a second from now',
|
'a second from now',
|
||||||
'30 seconds from now',
|
'30\xa0seconds from now',
|
||||||
'a minute from now',
|
'a minute from now',
|
||||||
'2 minutes from now',
|
'2\xa0minutes from now',
|
||||||
'an hour from now',
|
'an hour from now',
|
||||||
'23 hours from now',
|
'23\xa0hours from now',
|
||||||
'1 day from now',
|
'1\xa0day from now',
|
||||||
'2 days, 6 hours from now',
|
'2\xa0days, 6\xa0hours from now',
|
||||||
'1 year, 4 months from now',
|
'1\xa0year, 4\xa0months from now',
|
||||||
'now',
|
'now',
|
||||||
'now',
|
'now',
|
||||||
]
|
]
|
||||||
@ -218,8 +220,8 @@ class HumanizeTests(TestCase):
|
|||||||
# date in naive arithmetic is only 2 days and 5 hours after in
|
# date in naive arithmetic is only 2 days and 5 hours after in
|
||||||
# aware arithmetic.
|
# aware arithmetic.
|
||||||
result_list_with_tz_support = result_list[:]
|
result_list_with_tz_support = result_list[:]
|
||||||
assert result_list_with_tz_support[-4] == '2 days, 6 hours from now'
|
assert result_list_with_tz_support[-4] == '2\xa0days, 6\xa0hours from now'
|
||||||
result_list_with_tz_support[-4] == '2 days, 5 hours from now'
|
result_list_with_tz_support[-4] == '2\xa0days, 5\xa0hours from now'
|
||||||
|
|
||||||
orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime
|
orig_humanize_datetime, humanize.datetime = humanize.datetime, MockDateTime
|
||||||
try:
|
try:
|
||||||
|
@ -2,6 +2,8 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.utils.six.moves import cPickle as pickle
|
from django.utils.six.moves import cPickle as pickle
|
||||||
except ImportError:
|
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 get_random_string
|
||||||
from django.utils.crypto import salted_hmac
|
from django.utils.crypto import salted_hmac
|
||||||
from django.utils import timezone
|
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
|
# session_key should not be case sensitive because some backends can store it
|
||||||
# on case insensitive file systems.
|
# on case insensitive file systems.
|
||||||
@ -94,12 +98,16 @@ class SessionBase(object):
|
|||||||
hash, pickled = encoded_data.split(b':', 1)
|
hash, pickled = encoded_data.split(b':', 1)
|
||||||
expected_hash = self._hash(pickled)
|
expected_hash = self._hash(pickled)
|
||||||
if not constant_time_compare(hash.decode(), expected_hash):
|
if not constant_time_compare(hash.decode(), expected_hash):
|
||||||
raise SuspiciousOperation("Session data corrupted")
|
raise SuspiciousSession("Session data corrupted")
|
||||||
else:
|
else:
|
||||||
return pickle.loads(pickled)
|
return pickle.loads(pickled)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
# ValueError, SuspiciousOperation, unpickling exceptions. If any of
|
# ValueError, SuspiciousOperation, unpickling exceptions. If any of
|
||||||
# these happen, just return an empty dictionary (an empty session).
|
# 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 {}
|
return {}
|
||||||
|
|
||||||
def update(self, dict_):
|
def update(self, dict_):
|
||||||
|
@ -2,10 +2,13 @@
|
|||||||
Cached, database-backed sessions.
|
Cached, database-backed sessions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.contrib.sessions.backends.db import SessionStore as DBStore
|
from django.contrib.sessions.backends.db import SessionStore as DBStore
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
KEY_PREFIX = "django.contrib.sessions.cached_db"
|
KEY_PREFIX = "django.contrib.sessions.cached_db"
|
||||||
|
|
||||||
@ -41,7 +44,11 @@ class SessionStore(DBStore):
|
|||||||
data = self.decode(s.session_data)
|
data = self.decode(s.session_data)
|
||||||
cache.set(self.cache_key, data,
|
cache.set(self.cache_key, data,
|
||||||
self.get_expiry_age(expiry=s.expire_date))
|
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()
|
self.create()
|
||||||
data = {}
|
data = {}
|
||||||
return data
|
return data
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
from django.contrib.sessions.backends.base import SessionBase, CreateError
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.db import IntegrityError, transaction, router
|
from django.db import IntegrityError, transaction, router
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
class SessionStore(SessionBase):
|
class SessionStore(SessionBase):
|
||||||
"""
|
"""
|
||||||
@ -18,7 +20,11 @@ class SessionStore(SessionBase):
|
|||||||
expire_date__gt=timezone.now()
|
expire_date__gt=timezone.now()
|
||||||
)
|
)
|
||||||
return self.decode(s.session_data)
|
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()
|
self.create()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import errno
|
import errno
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tempfile
|
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.contrib.sessions.backends.base import SessionBase, CreateError, VALID_KEY_CHARS
|
||||||
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
|
from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
|
from django.contrib.sessions.exceptions import InvalidSessionKey
|
||||||
|
|
||||||
class SessionStore(SessionBase):
|
class SessionStore(SessionBase):
|
||||||
"""
|
"""
|
||||||
@ -48,7 +52,7 @@ class SessionStore(SessionBase):
|
|||||||
# should always be md5s, so they should never contain directory
|
# should always be md5s, so they should never contain directory
|
||||||
# components.
|
# components.
|
||||||
if not set(session_key).issubset(set(VALID_KEY_CHARS)):
|
if not set(session_key).issubset(set(VALID_KEY_CHARS)):
|
||||||
raise SuspiciousOperation(
|
raise InvalidSessionKey(
|
||||||
"Invalid characters in session key")
|
"Invalid characters in session key")
|
||||||
|
|
||||||
return os.path.join(self.storage_path, self.file_prefix + session_key)
|
return os.path.join(self.storage_path, self.file_prefix + session_key)
|
||||||
@ -75,7 +79,11 @@ class SessionStore(SessionBase):
|
|||||||
if file_data:
|
if file_data:
|
||||||
try:
|
try:
|
||||||
session_data = self.decode(file_data)
|
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()
|
self.create()
|
||||||
|
|
||||||
# Remove expired sessions.
|
# Remove expired sessions.
|
||||||
@ -86,7 +94,7 @@ class SessionStore(SessionBase):
|
|||||||
session_data = {}
|
session_data = {}
|
||||||
self.delete()
|
self.delete()
|
||||||
self.create()
|
self.create()
|
||||||
except IOError:
|
except (IOError, SuspiciousOperation):
|
||||||
self.create()
|
self.create()
|
||||||
return session_data
|
return session_data
|
||||||
|
|
||||||
|
11
django/contrib/sessions/exceptions.py
Normal file
11
django/contrib/sessions/exceptions.py
Normal file
@ -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
|
from datetime import timedelta
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
@ -15,14 +16,16 @@ from django.contrib.sessions.models import Session
|
|||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.core.cache import get_cache
|
from django.core.cache import get_cache
|
||||||
from django.core import management
|
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.http import HttpResponse
|
||||||
from django.test import TestCase, RequestFactory
|
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 six
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
|
|
||||||
|
from django.contrib.sessions.exceptions import InvalidSessionKey
|
||||||
|
|
||||||
|
|
||||||
class SessionTestsMixin(object):
|
class SessionTestsMixin(object):
|
||||||
# This does not inherit from TestCase to avoid any tests being run with this
|
# 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)
|
encoded = self.session.encode(data)
|
||||||
self.assertEqual(self.session.decode(encoded), 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):
|
def test_actual_expiry(self):
|
||||||
# Regression test for #19200
|
# Regression test for #19200
|
||||||
old_session_key = None
|
old_session_key = None
|
||||||
@ -403,14 +415,21 @@ class FileSessionTests(SessionTestsMixin, unittest.TestCase):
|
|||||||
self.assertRaises(ImproperlyConfigured, self.backend)
|
self.assertRaises(ImproperlyConfigured, self.backend)
|
||||||
|
|
||||||
def test_invalid_key_backslash(self):
|
def test_invalid_key_backslash(self):
|
||||||
# Ensure we don't allow directory-traversal
|
# This key should be refused and a new session should be created
|
||||||
self.assertRaises(SuspiciousOperation,
|
self.assertTrue(self.backend("a\\b\\c").load())
|
||||||
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):
|
def test_invalid_key_forwardslash(self):
|
||||||
# Ensure we don't allow directory-traversal
|
# Ensure we don't allow directory-traversal
|
||||||
self.assertRaises(SuspiciousOperation,
|
self.assertRaises(InvalidSessionKey,
|
||||||
self.backend("a/b/c").load)
|
self.backend()._key_to_file, "a/b/c")
|
||||||
|
|
||||||
@override_settings(SESSION_ENGINE="django.contrib.sessions.backends.file")
|
@override_settings(SESSION_ENGINE="django.contrib.sessions.backends.file")
|
||||||
def test_clearsessions_command(self):
|
def test_clearsessions_command(self):
|
||||||
|
@ -94,7 +94,7 @@ class Sitemap(object):
|
|||||||
'location': loc,
|
'location': loc,
|
||||||
'lastmod': self.__get('lastmod', item, None),
|
'lastmod': self.__get('lastmod', item, None),
|
||||||
'changefreq': self.__get('changefreq', 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)
|
urls.append(url_info)
|
||||||
return urls
|
return urls
|
||||||
|
@ -245,7 +245,7 @@ def find(path, all=False):
|
|||||||
if matches:
|
if matches:
|
||||||
return matches
|
return matches
|
||||||
# No match.
|
# No match.
|
||||||
return all and [] or None
|
return [] if all else None
|
||||||
|
|
||||||
|
|
||||||
def get_finders():
|
def get_finders():
|
||||||
|
@ -175,11 +175,9 @@ Type 'yes' to continue, or 'no' to cancel: """
|
|||||||
summary = template % {
|
summary = template % {
|
||||||
'modified_count': modified_count,
|
'modified_count': modified_count,
|
||||||
'identifier': 'static file' + ('' if modified_count == 1 else 's'),
|
'identifier': 'static file' + ('' if modified_count == 1 else 's'),
|
||||||
'action': self.symlink and 'symlinked' or 'copied',
|
'action': 'symlinked' if self.symlink else 'copied',
|
||||||
'destination': (destination_path and " to '%s'"
|
'destination': (" to '%s'" % destination_path if destination_path else ''),
|
||||||
% destination_path or ''),
|
'unmodified': (', %s unmodified' % unmodified_count if collected['unmodified'] else ''),
|
||||||
'unmodified': (collected['unmodified'] and ', %s unmodified'
|
|
||||||
% unmodified_count or ''),
|
|
||||||
'post_processed': (collected['post_processed'] and
|
'post_processed': (collected['post_processed'] and
|
||||||
', %s post-processed'
|
', %s post-processed'
|
||||||
% post_processed_count or ''),
|
% post_processed_count or ''),
|
||||||
|
10
django/core/cache/backends/base.py
vendored
10
django/core/cache/backends/base.py
vendored
@ -15,6 +15,10 @@ class CacheKeyWarning(DjangoRuntimeWarning):
|
|||||||
pass
|
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.
|
# Memcached does not accept keys longer than this.
|
||||||
MEMCACHE_MAX_KEY_LENGTH = 250
|
MEMCACHE_MAX_KEY_LENGTH = 250
|
||||||
|
|
||||||
@ -84,7 +88,7 @@ class BaseCache(object):
|
|||||||
new_key = self.key_func(key, self.key_prefix, version)
|
new_key = self.key_func(key, self.key_prefix, version)
|
||||||
return new_key
|
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
|
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
|
timeout is given, that timeout will be used for the key; otherwise
|
||||||
@ -101,7 +105,7 @@ class BaseCache(object):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
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
|
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.
|
used for the key; otherwise the default cache timeout will be used.
|
||||||
@ -163,7 +167,7 @@ class BaseCache(object):
|
|||||||
# if a subclass overrides it.
|
# if a subclass overrides it.
|
||||||
return self.has_key(key)
|
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
|
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
|
pairs. For certain backends (memcached), this is much more efficient
|
||||||
|
15
django/core/cache/backends/db.py
vendored
15
django/core/cache/backends/db.py
vendored
@ -9,7 +9,7 @@ except ImportError:
|
|||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
from django.conf import settings
|
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.db import connections, transaction, router, DatabaseError
|
||||||
from django.utils import timezone, six
|
from django.utils import timezone, six
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
@ -65,6 +65,7 @@ class DatabaseCache(BaseDatabaseCache):
|
|||||||
if row is None:
|
if row is None:
|
||||||
return default
|
return default
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
if row[2] < now:
|
if row[2] < now:
|
||||||
db = router.db_for_write(self.cache_model_class)
|
db = router.db_for_write(self.cache_model_class)
|
||||||
cursor = connections[db].cursor()
|
cursor = connections[db].cursor()
|
||||||
@ -74,18 +75,18 @@ class DatabaseCache(BaseDatabaseCache):
|
|||||||
value = connections[db].ops.process_clob(row[1])
|
value = connections[db].ops.process_clob(row[1])
|
||||||
return pickle.loads(base64.b64decode(force_bytes(value)))
|
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)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
self._base_set('set', key, value, timeout)
|
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)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
return self._base_set('add', key, value, timeout)
|
return self._base_set('add', key, value, timeout)
|
||||||
|
|
||||||
def _base_set(self, mode, key, value, timeout=None):
|
def _base_set(self, mode, key, value, timeout=DEFAULT_TIMEOUT):
|
||||||
if timeout is None:
|
if timeout == DEFAULT_TIMEOUT:
|
||||||
timeout = self.default_timeout
|
timeout = self.default_timeout
|
||||||
db = router.db_for_write(self.cache_model_class)
|
db = router.db_for_write(self.cache_model_class)
|
||||||
table = connections[db].ops.quote_name(self._table)
|
table = connections[db].ops.quote_name(self._table)
|
||||||
@ -95,7 +96,9 @@ class DatabaseCache(BaseDatabaseCache):
|
|||||||
num = cursor.fetchone()[0]
|
num = cursor.fetchone()[0]
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
now = now.replace(microsecond=0)
|
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)
|
exp = datetime.utcfromtimestamp(time.time() + timeout)
|
||||||
else:
|
else:
|
||||||
exp = datetime.fromtimestamp(time.time() + timeout)
|
exp = datetime.fromtimestamp(time.time() + timeout)
|
||||||
|
8
django/core/cache/backends/dummy.py
vendored
8
django/core/cache/backends/dummy.py
vendored
@ -1,12 +1,12 @@
|
|||||||
"Dummy cache backend"
|
"Dummy cache backend"
|
||||||
|
|
||||||
from django.core.cache.backends.base import BaseCache
|
from django.core.cache.backends.base import BaseCache, DEFAULT_TIMEOUT
|
||||||
|
|
||||||
class DummyCache(BaseCache):
|
class DummyCache(BaseCache):
|
||||||
def __init__(self, host, *args, **kwargs):
|
def __init__(self, host, *args, **kwargs):
|
||||||
BaseCache.__init__(self, *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)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
return True
|
return True
|
||||||
@ -16,7 +16,7 @@ class DummyCache(BaseCache):
|
|||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
return default
|
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)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ class DummyCache(BaseCache):
|
|||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def set_many(self, data, timeout=0, version=None):
|
def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def delete_many(self, keys, version=None):
|
def delete_many(self, keys, version=None):
|
||||||
|
15
django/core/cache/backends/filebased.py
vendored
15
django/core/cache/backends/filebased.py
vendored
@ -9,9 +9,10 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import pickle
|
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
|
from django.utils.encoding import force_bytes
|
||||||
|
|
||||||
|
|
||||||
class FileBasedCache(BaseCache):
|
class FileBasedCache(BaseCache):
|
||||||
def __init__(self, dir, params):
|
def __init__(self, dir, params):
|
||||||
BaseCache.__init__(self, params)
|
BaseCache.__init__(self, params)
|
||||||
@ -19,7 +20,7 @@ class FileBasedCache(BaseCache):
|
|||||||
if not os.path.exists(self._dir):
|
if not os.path.exists(self._dir):
|
||||||
self._createdir()
|
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):
|
if self.has_key(key, version=version):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ class FileBasedCache(BaseCache):
|
|||||||
with open(fname, 'rb') as f:
|
with open(fname, 'rb') as f:
|
||||||
exp = pickle.load(f)
|
exp = pickle.load(f)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if exp < now:
|
if exp is not None and exp < now:
|
||||||
self._delete(fname)
|
self._delete(fname)
|
||||||
else:
|
else:
|
||||||
return pickle.load(f)
|
return pickle.load(f)
|
||||||
@ -43,14 +44,14 @@ class FileBasedCache(BaseCache):
|
|||||||
pass
|
pass
|
||||||
return default
|
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)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
|
|
||||||
fname = self._key_to_file(key)
|
fname = self._key_to_file(key)
|
||||||
dirname = os.path.dirname(fname)
|
dirname = os.path.dirname(fname)
|
||||||
|
|
||||||
if timeout is None:
|
if timeout == DEFAULT_TIMEOUT:
|
||||||
timeout = self.default_timeout
|
timeout = self.default_timeout
|
||||||
|
|
||||||
self._cull()
|
self._cull()
|
||||||
@ -60,8 +61,8 @@ class FileBasedCache(BaseCache):
|
|||||||
os.makedirs(dirname)
|
os.makedirs(dirname)
|
||||||
|
|
||||||
with open(fname, 'wb') as f:
|
with open(fname, 'wb') as f:
|
||||||
now = time.time()
|
expiry = None if timeout is None else time.time() + timeout
|
||||||
pickle.dump(now + timeout, f, pickle.HIGHEST_PROTOCOL)
|
pickle.dump(expiry, f, pickle.HIGHEST_PROTOCOL)
|
||||||
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
|
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
pass
|
pass
|
||||||
|
19
django/core/cache/backends/locmem.py
vendored
19
django/core/cache/backends/locmem.py
vendored
@ -6,7 +6,7 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
import pickle
|
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
|
from django.utils.synch import RWLock
|
||||||
|
|
||||||
# Global in-memory store of cache data. Keyed by name, to provide
|
# 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._expire_info = _expire_info.setdefault(name, {})
|
||||||
self._lock = _locks.setdefault(name, RWLock())
|
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)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
with self._lock.writer():
|
with self._lock.writer():
|
||||||
@ -41,10 +41,8 @@ class LocMemCache(BaseCache):
|
|||||||
key = self.make_key(key, version=version)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
with self._lock.reader():
|
with self._lock.reader():
|
||||||
exp = self._expire_info.get(key)
|
exp = self._expire_info.get(key, 0)
|
||||||
if exp is None:
|
if exp is None or exp > time.time():
|
||||||
return default
|
|
||||||
elif exp > time.time():
|
|
||||||
try:
|
try:
|
||||||
pickled = self._cache[key]
|
pickled = self._cache[key]
|
||||||
return pickle.loads(pickled)
|
return pickle.loads(pickled)
|
||||||
@ -58,15 +56,16 @@ class LocMemCache(BaseCache):
|
|||||||
pass
|
pass
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def _set(self, key, value, timeout=None):
|
def _set(self, key, value, timeout=DEFAULT_TIMEOUT):
|
||||||
if len(self._cache) >= self._max_entries:
|
if len(self._cache) >= self._max_entries:
|
||||||
self._cull()
|
self._cull()
|
||||||
if timeout is None:
|
if timeout == DEFAULT_TIMEOUT:
|
||||||
timeout = self.default_timeout
|
timeout = self.default_timeout
|
||||||
|
expiry = None if timeout is None else time.time() + timeout
|
||||||
self._cache[key] = value
|
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)
|
key = self.make_key(key, version=version)
|
||||||
self.validate_key(key)
|
self.validate_key(key)
|
||||||
with self._lock.writer():
|
with self._lock.writer():
|
||||||
|
22
django/core/cache/backends/memcached.py
vendored
22
django/core/cache/backends/memcached.py
vendored
@ -4,7 +4,7 @@ import time
|
|||||||
import pickle
|
import pickle
|
||||||
from threading import local
|
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 import six
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
@ -36,12 +36,22 @@ class BaseMemcachedCache(BaseCache):
|
|||||||
|
|
||||||
return self._client
|
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
|
Memcached deals with long (> 30 days) timeouts in a special
|
||||||
way. Call this function to obtain a safe value for your timeout.
|
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
|
if timeout > 2592000: # 60*60*24*30, 30 days
|
||||||
# See http://code.google.com/p/memcached/wiki/FAQ
|
# See http://code.google.com/p/memcached/wiki/FAQ
|
||||||
# "You can set expire times up to 30 days in the future. After that
|
# "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.
|
# Python 2 memcache requires the key to be a byte string.
|
||||||
return force_str(super(BaseMemcachedCache, self).make_key(key, version))
|
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)
|
key = self.make_key(key, version=version)
|
||||||
return self._cache.add(key, value, self._get_memcache_timeout(timeout))
|
return self._cache.add(key, value, self._get_memcache_timeout(timeout))
|
||||||
|
|
||||||
@ -67,7 +77,7 @@ class BaseMemcachedCache(BaseCache):
|
|||||||
return default
|
return default
|
||||||
return val
|
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)
|
key = self.make_key(key, version=version)
|
||||||
self._cache.set(key, value, self._get_memcache_timeout(timeout))
|
self._cache.set(key, value, self._get_memcache_timeout(timeout))
|
||||||
|
|
||||||
@ -125,7 +135,7 @@ class BaseMemcachedCache(BaseCache):
|
|||||||
raise ValueError("Key '%s' not found" % key)
|
raise ValueError("Key '%s' not found" % key)
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def set_many(self, data, timeout=0, version=None):
|
def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
|
||||||
safe_data = {}
|
safe_data = {}
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
key = self.make_key(key, version=version)
|
key = self.make_key(key, version=version)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Global Django exception and warning classes.
|
Global Django exception and warning classes.
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
@ -9,37 +10,56 @@ class DjangoRuntimeWarning(RuntimeWarning):
|
|||||||
|
|
||||||
|
|
||||||
class ObjectDoesNotExist(Exception):
|
class ObjectDoesNotExist(Exception):
|
||||||
"The requested object does not exist"
|
"""The requested object does not exist"""
|
||||||
silent_variable_failure = True
|
silent_variable_failure = True
|
||||||
|
|
||||||
|
|
||||||
class MultipleObjectsReturned(Exception):
|
class MultipleObjectsReturned(Exception):
|
||||||
"The query returned multiple objects when only one was expected."
|
"""The query returned multiple objects when only one was expected."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SuspiciousOperation(Exception):
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PermissionDenied(Exception):
|
class PermissionDenied(Exception):
|
||||||
"The user did not have permission to do that"
|
"""The user did not have permission to do that"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ViewDoesNotExist(Exception):
|
class ViewDoesNotExist(Exception):
|
||||||
"The requested view does not exist"
|
"""The requested view does not exist"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MiddlewareNotUsed(Exception):
|
class MiddlewareNotUsed(Exception):
|
||||||
"This middleware is not used in this server configuration"
|
"""This middleware is not used in this server configuration"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ImproperlyConfigured(Exception):
|
class ImproperlyConfigured(Exception):
|
||||||
"Django is somehow improperly configured"
|
"""Django is somehow improperly configured"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ except (ImportError, AttributeError):
|
|||||||
|
|
||||||
def fd(f):
|
def fd(f):
|
||||||
"""Get a filedescriptor from something which could be a file or an fd."""
|
"""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':
|
if system_type == 'nt':
|
||||||
def lock(file, flags):
|
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:
|
with open(old_file_name, 'rb') as old_file:
|
||||||
# now open the new file, not forgetting allow_overwrite
|
# 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) |
|
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:
|
try:
|
||||||
locks.lock(fd, locks.LOCK_EX)
|
locks.lock(fd, locks.LOCK_EX)
|
||||||
current_chunk = None
|
current_chunk = None
|
||||||
|
@ -8,7 +8,7 @@ import itertools
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
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 import locks, File
|
||||||
from django.core.files.move import file_move_safe
|
from django.core.files.move import file_move_safe
|
||||||
from django.utils.encoding import force_text, filepath_to_uri
|
from django.utils.encoding import force_text, filepath_to_uri
|
||||||
@ -260,7 +260,7 @@ class FileSystemStorage(Storage):
|
|||||||
try:
|
try:
|
||||||
path = safe_join(self.location, name)
|
path = safe_join(self.location, name)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise SuspiciousOperation("Attempted access to '%s' denied." % name)
|
raise SuspiciousFileOperation("Attempted access to '%s' denied." % name)
|
||||||
return os.path.normpath(path)
|
return os.path.normpath(path)
|
||||||
|
|
||||||
def size(self, name):
|
def size(self, name):
|
||||||
|
@ -8,7 +8,7 @@ from django import http
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import urlresolvers
|
from django.core import urlresolvers
|
||||||
from django.core import signals
|
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.db import connections, transaction
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.module_loading import import_by_path
|
from django.utils.module_loading import import_by_path
|
||||||
@ -66,10 +66,11 @@ class BaseHandler(object):
|
|||||||
self._request_middleware = request_middleware
|
self._request_middleware = request_middleware
|
||||||
|
|
||||||
def make_view_atomic(self, view):
|
def make_view_atomic(self, view):
|
||||||
if getattr(view, 'transactions_per_request', True):
|
non_atomic_requests = getattr(view, '_non_atomic_requests', set())
|
||||||
for db in connections.all():
|
for db in connections.all():
|
||||||
if db.settings_dict['ATOMIC_REQUESTS']:
|
if (db.settings_dict['ATOMIC_REQUESTS']
|
||||||
view = transaction.atomic(using=db.alias)(view)
|
and db.alias not in non_atomic_requests):
|
||||||
|
view = transaction.atomic(using=db.alias)(view)
|
||||||
return view
|
return view
|
||||||
|
|
||||||
def get_response(self, request):
|
def get_response(self, request):
|
||||||
@ -169,11 +170,27 @@ class BaseHandler(object):
|
|||||||
response = self.handle_uncaught_exception(request,
|
response = self.handle_uncaught_exception(request,
|
||||||
resolver, sys.exc_info())
|
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:
|
except SystemExit:
|
||||||
# Allow sys.exit() to actually exit. See tickets #1023 and #4701
|
# Allow sys.exit() to actually exit. See tickets #1023 and #4701
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except: # Handle everything else, including SuspiciousOperation, etc.
|
except: # Handle everything else.
|
||||||
# Get the exception info now, in case another exception is thrown later.
|
# Get the exception info now, in case another exception is thrown later.
|
||||||
signals.got_request_exception.send(sender=self.__class__, request=request)
|
signals.got_request_exception.send(sender=self.__class__, request=request)
|
||||||
response = self.handle_uncaught_exception(request, resolver, sys.exc_info())
|
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 import datastructures
|
||||||
from django.utils.encoding import force_str, force_text, iri_to_uri
|
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')
|
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):
|
class LimitedStream(object):
|
||||||
'''
|
'''
|
||||||
LimitedStream wraps another stream in order to not allow reading from it
|
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__
|
response._handler_class = self.__class__
|
||||||
|
|
||||||
try:
|
status = '%s %s' % (response.status_code, response.reason_phrase)
|
||||||
status_text = STATUS_CODE_TEXT[response.status_code]
|
|
||||||
except KeyError:
|
|
||||||
status_text = 'UNKNOWN STATUS CODE'
|
|
||||||
status = '%s %s' % (response.status_code, status_text)
|
|
||||||
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
response_headers = [(str(k), str(v)) for k, v in response.items()]
|
||||||
for c in response.cookies.values():
|
for c in response.cookies.values():
|
||||||
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
response_headers.append((str('Set-Cookie'), str(c.output(header=''))))
|
||||||
|
@ -7,7 +7,6 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from optparse import make_option, OptionParser
|
from optparse import make_option, OptionParser
|
||||||
import traceback
|
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
@ -171,7 +170,7 @@ class BaseCommand(object):
|
|||||||
make_option('--pythonpath',
|
make_option('--pythonpath',
|
||||||
help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'),
|
help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'),
|
||||||
make_option('--traceback', action='store_true',
|
make_option('--traceback', action='store_true',
|
||||||
help='Print traceback on exception'),
|
help='Raise on exception'),
|
||||||
)
|
)
|
||||||
help = ''
|
help = ''
|
||||||
args = ''
|
args = ''
|
||||||
@ -231,7 +230,8 @@ class BaseCommand(object):
|
|||||||
Set up any environment changes requested (e.g., Python path
|
Set up any environment changes requested (e.g., Python path
|
||||||
and Django settings), then run this command. If the
|
and Django settings), then run this command. If the
|
||||||
command raises a ``CommandError``, intercept it and print it sensibly
|
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])
|
parser = self.create_parser(argv[0], argv[1])
|
||||||
options, args = parser.parse_args(argv[2:])
|
options, args = parser.parse_args(argv[2:])
|
||||||
@ -239,12 +239,12 @@ class BaseCommand(object):
|
|||||||
try:
|
try:
|
||||||
self.execute(*args, **options.__dict__)
|
self.execute(*args, **options.__dict__)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if options.traceback or not isinstance(e, CommandError):
|
||||||
|
raise
|
||||||
|
|
||||||
# self.stderr is not guaranteed to be set here
|
# self.stderr is not guaranteed to be set here
|
||||||
stderr = getattr(self, 'stderr', OutputWrapper(sys.stderr, self.style.ERROR))
|
stderr = getattr(self, 'stderr', OutputWrapper(sys.stderr, self.style.ERROR))
|
||||||
if options.traceback or not isinstance(e, CommandError):
|
stderr.write('%s: %s' % (e.__class__.__name__, e))
|
||||||
stderr.write(traceback.format_exc())
|
|
||||||
else:
|
|
||||||
stderr.write('%s: %s' % (e.__class__.__name__, e))
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
def execute(self, *args, **options):
|
def execute(self, *args, **options):
|
||||||
|
@ -38,7 +38,7 @@ class Command(LabelCommand):
|
|||||||
qn = connection.ops.quote_name
|
qn = connection.ops.quote_name
|
||||||
for f in fields:
|
for f in fields:
|
||||||
field_output = [qn(f.name), f.db_type(connection=connection)]
|
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:
|
if f.primary_key:
|
||||||
field_output.append("PRIMARY KEY")
|
field_output.append("PRIMARY KEY")
|
||||||
elif f.unique:
|
elif f.unique:
|
||||||
@ -51,7 +51,7 @@ class Command(LabelCommand):
|
|||||||
table_output.append(" ".join(field_output))
|
table_output.append(" ".join(field_output))
|
||||||
full_statement = ["CREATE TABLE %s (" % qn(tablename)]
|
full_statement = ["CREATE TABLE %s (" % qn(tablename)]
|
||||||
for i, line in enumerate(table_output):
|
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(');')
|
full_statement.append(');')
|
||||||
with transaction.commit_on_success_unless_managed():
|
with transaction.commit_on_success_unless_managed():
|
||||||
curs = connection.cursor()
|
curs = connection.cursor()
|
||||||
|
@ -21,6 +21,9 @@ class Command(BaseCommand):
|
|||||||
help='Use natural keys if they are available.'),
|
help='Use natural keys if they are available.'),
|
||||||
make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False,
|
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."),
|
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 "
|
help = ("Output the contents of the database as a fixture of the given "
|
||||||
"format (using each model's default manager unless --all is "
|
"format (using each model's default manager unless --all is "
|
||||||
@ -37,6 +40,12 @@ class Command(BaseCommand):
|
|||||||
show_traceback = options.get('traceback')
|
show_traceback = options.get('traceback')
|
||||||
use_natural_keys = options.get('use_natural_keys')
|
use_natural_keys = options.get('use_natural_keys')
|
||||||
use_base_manager = options.get('use_base_manager')
|
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_apps = set()
|
||||||
excluded_models = set()
|
excluded_models = set()
|
||||||
@ -55,8 +64,12 @@ class Command(BaseCommand):
|
|||||||
raise CommandError('Unknown app in excludes: %s' % exclude)
|
raise CommandError('Unknown app in excludes: %s' % exclude)
|
||||||
|
|
||||||
if len(app_labels) == 0:
|
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)
|
app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps)
|
||||||
else:
|
else:
|
||||||
|
if len(app_labels) > 1 and primary_keys:
|
||||||
|
raise CommandError("You can only use --pks option with one model")
|
||||||
app_list = SortedDict()
|
app_list = SortedDict()
|
||||||
for label in app_labels:
|
for label in app_labels:
|
||||||
try:
|
try:
|
||||||
@ -77,6 +90,8 @@ class Command(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
app_list[app] = [model]
|
app_list[app] = [model]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
if primary_keys:
|
||||||
|
raise CommandError("You can only use --pks option with one model")
|
||||||
# This is just an app - no model qualifier
|
# This is just an app - no model qualifier
|
||||||
app_label = label
|
app_label = label
|
||||||
try:
|
try:
|
||||||
@ -107,8 +122,11 @@ class Command(BaseCommand):
|
|||||||
objects = model._base_manager
|
objects = model._base_manager
|
||||||
else:
|
else:
|
||||||
objects = model._default_manager
|
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
|
yield obj
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -20,7 +20,7 @@ class Command(NoArgsCommand):
|
|||||||
default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. '
|
default=DEFAULT_DB_ALIAS, help='Nominates a database to flush. '
|
||||||
'Defaults to the "default" database.'),
|
'Defaults to the "default" database.'),
|
||||||
make_option('--no-initial-data', action='store_false', dest='load_initial_data', default=True,
|
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 '
|
help = ('Returns the database to the state it was in immediately after '
|
||||||
'syncdb was executed. This means that all data will be removed '
|
'syncdb was executed. This means that all data will be removed '
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import os
|
import glob
|
||||||
import gzip
|
import gzip
|
||||||
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
from optparse import make_option
|
from optparse import make_option
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import serializers
|
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.core.management.color import no_style
|
||||||
from django.db import (connections, router, transaction, DEFAULT_DB_ALIAS,
|
from django.db import (connections, router, transaction, DEFAULT_DB_ALIAS,
|
||||||
IntegrityError, DatabaseError)
|
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.encoding import force_text
|
||||||
|
from django.utils.functional import cached_property, memoize
|
||||||
from django.utils._os import upath
|
from django.utils._os import upath
|
||||||
from itertools import product
|
from itertools import product
|
||||||
|
|
||||||
@ -43,9 +46,8 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
if not len(fixture_labels):
|
if not len(fixture_labels):
|
||||||
raise CommandError(
|
raise CommandError(
|
||||||
"No database fixture specified. Please provide the path of at "
|
"No database fixture specified. Please provide the path "
|
||||||
"least one fixture in the command line."
|
"of at least one fixture in the command line.")
|
||||||
)
|
|
||||||
|
|
||||||
self.verbosity = int(options.get('verbosity'))
|
self.verbosity = int(options.get('verbosity'))
|
||||||
|
|
||||||
@ -68,37 +70,18 @@ class Command(BaseCommand):
|
|||||||
self.fixture_object_count = 0
|
self.fixture_object_count = 0
|
||||||
self.models = set()
|
self.models = set()
|
||||||
|
|
||||||
class SingleZipReader(zipfile.ZipFile):
|
self.serialization_formats = serializers.get_public_serializer_formats()
|
||||||
def __init__(self, *args, **kwargs):
|
self.compression_formats = {
|
||||||
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 = {
|
|
||||||
None: open,
|
None: open,
|
||||||
'gz': gzip.GzipFile,
|
'gz': gzip.GzipFile,
|
||||||
'zip': SingleZipReader
|
'zip': SingleZipReader
|
||||||
}
|
}
|
||||||
if has_bz2:
|
if has_bz2:
|
||||||
self.compression_types['bz2'] = bz2.BZ2File
|
self.compression_formats['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]
|
|
||||||
|
|
||||||
with connection.constraint_checks_disabled():
|
with connection.constraint_checks_disabled():
|
||||||
for fixture_label in fixture_labels:
|
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
|
# Since we disabled constraint checks, we must manually check for
|
||||||
# any invalid keys that might have been added
|
# any invalid keys that might have been added
|
||||||
@ -123,122 +106,174 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
if self.verbosity >= 1:
|
if self.verbosity >= 1:
|
||||||
if self.fixture_object_count == self.loaded_object_count:
|
if self.fixture_object_count == self.loaded_object_count:
|
||||||
self.stdout.write("Installed %d object(s) from %d fixture(s)" % (
|
self.stdout.write("Installed %d object(s) from %d fixture(s)" %
|
||||||
self.loaded_object_count, self.fixture_count))
|
(self.loaded_object_count, self.fixture_count))
|
||||||
else:
|
else:
|
||||||
self.stdout.write("Installed %d object(s) (of %d) from %d fixture(s)" % (
|
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.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:
|
for obj in objects:
|
||||||
compression_formats = [parts[-1]]
|
objects_in_fixture += 1
|
||||||
parts = parts[:-1]
|
if router.allow_syncdb(self.using, obj.object.__class__):
|
||||||
else:
|
loaded_objects_in_fixture += 1
|
||||||
compression_formats = self.compression_types.keys()
|
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:
|
self.loaded_object_count += loaded_objects_in_fixture
|
||||||
fixture_name = parts[0]
|
self.fixture_object_count += objects_in_fixture
|
||||||
formats = serializers.get_public_serializer_formats()
|
except Exception as e:
|
||||||
else:
|
if not isinstance(e, CommandError):
|
||||||
fixture_name, format = '.'.join(parts[:-1]), parts[-1]
|
e.args = ("Problem installing fixture '%s': %s" % (fixture_file, e),)
|
||||||
if format in serializers.get_public_serializer_formats():
|
raise
|
||||||
formats = [format]
|
finally:
|
||||||
else:
|
fixture.close()
|
||||||
formats = []
|
|
||||||
|
|
||||||
if formats:
|
# If the fixture we loaded contains 0 objects, assume that an
|
||||||
if self.verbosity >= 2:
|
# error was encountered during fixture loading.
|
||||||
self.stdout.write("Loading '%s' fixtures..." % fixture_name)
|
if objects_in_fixture == 0:
|
||||||
else:
|
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(
|
raise CommandError(
|
||||||
"Problem installing fixture '%s': %s is not a known serialization format." %
|
"Problem installing fixture '%s': %s is not a known "
|
||||||
(fixture_name, format))
|
"serialization format." % tuple(fixture_name.rsplit('.')))
|
||||||
|
|
||||||
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'
|
|
||||||
|
|
||||||
if self.verbosity >= 2:
|
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
|
if os.path.isabs(fixture_name):
|
||||||
for combo in product([self.using, None], serialization_formats, compression_formats):
|
fixture_dirs = [os.path.dirname(fixture_name)]
|
||||||
database, format, compression_format = combo
|
fixture_name = os.path.basename(fixture_name)
|
||||||
file_name = '.'.join(
|
else:
|
||||||
p for p in [
|
fixture_dirs = self.fixture_dirs
|
||||||
fixture_name, database, format, compression_format
|
|
||||||
]
|
|
||||||
if p
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.verbosity >= 3:
|
suffixes = ('.'.join(ext for ext in combo if ext)
|
||||||
self.stdout.write("Trying %s for %s fixture '%s'..." % \
|
for combo in product(databases, ser_fmts, cmp_fmts))
|
||||||
(humanize(fixture_dir), file_name, fixture_name))
|
targets = set('.'.join((fixture_name, suffix)) for suffix in suffixes)
|
||||||
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)))
|
|
||||||
|
|
||||||
self.fixture_count += 1
|
fixture_files = []
|
||||||
objects_in_fixture = 0
|
for fixture_dir in fixture_dirs:
|
||||||
loaded_objects_in_fixture = 0
|
if self.verbosity >= 2:
|
||||||
if self.verbosity >= 2:
|
self.stdout.write("Checking %s for fixtures..." % humanize(fixture_dir))
|
||||||
self.stdout.write("Installing %s fixture '%s' from %s." % \
|
fixture_files_in_dir = []
|
||||||
(format, fixture_name, humanize(fixture_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:
|
# Check kept for backwards-compatibility; it isn't clear why
|
||||||
objects_in_fixture += 1
|
# duplicates are only allowed in different directories.
|
||||||
if router.allow_syncdb(self.using, obj.object.__class__):
|
if len(fixture_files_in_dir) > 1:
|
||||||
loaded_objects_in_fixture += 1
|
raise CommandError(
|
||||||
self.models.add(obj.object.__class__)
|
"Multiple fixtures named '%s' in %s. Aborting." %
|
||||||
try:
|
(fixture_name, humanize(fixture_dir)))
|
||||||
obj.save(using=self.using)
|
fixture_files.extend(fixture_files_in_dir)
|
||||||
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
|
|
||||||
|
|
||||||
self.loaded_object_count += loaded_objects_in_fixture
|
if fixture_name != 'initial_data' and not fixture_files:
|
||||||
self.fixture_object_count += objects_in_fixture
|
# Warning kept for backwards-compatibility; why not an exception?
|
||||||
label_found = True
|
warnings.warn("No fixture named '%s' found." % fixture_name)
|
||||||
except Exception as e:
|
|
||||||
if not isinstance(e, CommandError):
|
|
||||||
e.args = ("Problem installing fixture '%s': %s" % (full_path, e),)
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
fixture.close()
|
|
||||||
|
|
||||||
# If the fixture we loaded contains 0 objects, assume that an
|
return fixture_files
|
||||||
# error was encountered during fixture loading.
|
|
||||||
if objects_in_fixture == 0:
|
_label_to_fixtures_cache = {}
|
||||||
raise CommandError(
|
find_fixtures = memoize(_find_fixtures, _label_to_fixtures_cache, 2)
|
||||||
"No fixture data found for '%s'. (File format may be invalid.)" %
|
|
||||||
(fixture_name))
|
@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.")
|
"if you want to enable i18n for your project or application.")
|
||||||
|
|
||||||
check_programs('xgettext')
|
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)
|
potfile = self.build_pot_file(localedir)
|
||||||
|
|
||||||
@ -309,10 +297,9 @@ class Command(NoArgsCommand):
|
|||||||
"""
|
"""
|
||||||
Check if the given path should be ignored or not.
|
Check if the given path should be ignored or not.
|
||||||
"""
|
"""
|
||||||
for pattern in ignore_patterns:
|
filename = os.path.basename(path)
|
||||||
if fnmatch.fnmatchcase(path, pattern):
|
ignore = lambda pattern: fnmatch.fnmatchcase(filename, pattern)
|
||||||
return True
|
return any(ignore(pattern) for pattern in ignore_patterns)
|
||||||
return False
|
|
||||||
|
|
||||||
dir_suffix = '%s*' % os.sep
|
dir_suffix = '%s*' % os.sep
|
||||||
norm_patterns = [p[:-len(dir_suffix)] if p.endswith(dir_suffix) else p for p in self.ignore_patterns]
|
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'),
|
"started_at": datetime.now().strftime('%B %d, %Y - %X'),
|
||||||
"version": self.get_version(),
|
"version": self.get_version(),
|
||||||
"settings": settings.SETTINGS_MODULE,
|
"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,
|
"port": self.port,
|
||||||
"quit_command": quit_command,
|
"quit_command": quit_command,
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
from optparse import make_option
|
from optparse import make_option
|
||||||
|
import itertools
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.management.base import NoArgsCommand
|
from django.core.management.base import NoArgsCommand
|
||||||
from django.core.management.color import no_style
|
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.db import connections, router, transaction, models, DEFAULT_DB_ALIAS
|
||||||
from django.utils.datastructures import SortedDict
|
from django.utils.datastructures import SortedDict
|
||||||
from django.utils.importlib import import_module
|
from django.utils.importlib import import_module
|
||||||
@ -82,6 +83,9 @@ class Command(NoArgsCommand):
|
|||||||
for app_name, model_list in all_models
|
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
|
# Create the tables for each model
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
self.stdout.write("Creating tables ...\n")
|
self.stdout.write("Creating tables ...\n")
|
||||||
|
@ -29,7 +29,7 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
help = ('Runs the test suite for the specified applications, or the '
|
help = ('Runs the test suite for the specified applications, or the '
|
||||||
'entire site if no apps are specified.')
|
'entire site if no apps are specified.')
|
||||||
args = '[appname ...]'
|
args = '[appname|appname.tests.TestCase|appname.tests.TestCase.test_method]...'
|
||||||
|
|
||||||
requires_model_validation = False
|
requires_model_validation = False
|
||||||
|
|
||||||
|
@ -133,14 +133,15 @@ def sql_custom(app, style, connection):
|
|||||||
def sql_indexes(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."
|
"Returns a list of the CREATE INDEX SQL statements for all models in the given app."
|
||||||
output = []
|
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))
|
output.extend(connection.creation.sql_indexes_for_model(model, style))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
def sql_destroy_indexes(app, style, connection):
|
def sql_destroy_indexes(app, style, connection):
|
||||||
"Returns a list of the DROP INDEX SQL statements for all models in the given app."
|
"Returns a list of the DROP INDEX SQL statements for all models in the given app."
|
||||||
output = []
|
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))
|
output.extend(connection.creation.sql_destroy_indexes_for_model(model, style))
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@ -191,6 +192,19 @@ def custom_sql_for_model(model, style, connection):
|
|||||||
return output
|
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):
|
def emit_post_sync_signal(created_models, verbosity, interactive, db):
|
||||||
# Emit the post_sync signal for every application.
|
# Emit the post_sync signal for every application.
|
||||||
for app in models.get_apps():
|
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)
|
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):
|
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)
|
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 f.choices:
|
||||||
if isinstance(f.choices, six.string_types) or not is_iterable(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)
|
e.add(opts, '"%s": "choices" should be iterable (e.g., a tuple or list).' % f.name)
|
||||||
else:
|
else:
|
||||||
for c in f.choices:
|
for c in f.choices:
|
||||||
if not isinstance(c, (list, tuple)) or len(c) != 2:
|
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-tuples.' % f.name)
|
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):
|
if f.db_index not in (None, True, False):
|
||||||
e.add(opts, '"%s": "db_index" should be either None, True or False.' % f.name)
|
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
|
raise TypeError
|
||||||
# The object_list is converted to a list so that if it was a QuerySet
|
# The object_list is converted to a list so that if it was a QuerySet
|
||||||
# it won't be a database hit per __getitem__.
|
# 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):
|
def has_next(self):
|
||||||
return self.number < self.paginator.num_pages
|
return self.number < self.paginator.num_pages
|
||||||
|
@ -75,8 +75,7 @@ class Resolver404(Http404):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class NoReverseMatch(Exception):
|
class NoReverseMatch(Exception):
|
||||||
# Don't make this raise an error when used in a template.
|
pass
|
||||||
silent_variable_failure = True
|
|
||||||
|
|
||||||
def get_callable(lookup_view, can_fail=False):
|
def get_callable(lookup_view, can_fail=False):
|
||||||
"""
|
"""
|
||||||
@ -360,6 +359,9 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||||||
callback = getattr(urls, 'handler%s' % view_type)
|
callback = getattr(urls, 'handler%s' % view_type)
|
||||||
return get_callable(callback), {}
|
return get_callable(callback), {}
|
||||||
|
|
||||||
|
def resolve400(self):
|
||||||
|
return self._resolve_special('400')
|
||||||
|
|
||||||
def resolve403(self):
|
def resolve403(self):
|
||||||
return self._resolve_special('403')
|
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
|
import warnings
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core import signals
|
from django.core import signals
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from django.db.utils import (DEFAULT_DB_ALIAS,
|
from django.db.utils import (DEFAULT_DB_ALIAS,
|
||||||
DataError, OperationalError, IntegrityError, InternalError,
|
DataError, OperationalError, IntegrityError, InternalError,
|
||||||
ProgrammingError, NotSupportedError, DatabaseError,
|
ProgrammingError, NotSupportedError, DatabaseError,
|
||||||
InterfaceError, Error,
|
InterfaceError, Error,
|
||||||
load_backend, ConnectionHandler, ConnectionRouter)
|
load_backend, ConnectionHandler, ConnectionRouter)
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
|
__all__ = ('backend', 'connection', 'connections', 'router', 'DatabaseError',
|
||||||
'IntegrityError', 'DEFAULT_DB_ALIAS')
|
'IntegrityError', 'DEFAULT_DB_ALIAS')
|
||||||
|
|
||||||
|
connections = ConnectionHandler()
|
||||||
|
|
||||||
if settings.DATABASES and DEFAULT_DB_ALIAS not in settings.DATABASES:
|
router = ConnectionRouter()
|
||||||
raise ImproperlyConfigured("You must define a '%s' database" % DEFAULT_DB_ALIAS)
|
|
||||||
|
|
||||||
connections = ConnectionHandler(settings.DATABASES)
|
|
||||||
|
|
||||||
router = ConnectionRouter(settings.DATABASE_ROUTERS)
|
|
||||||
|
|
||||||
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
|
# `connection`, `DatabaseError` and `IntegrityError` are convenient aliases
|
||||||
# for backend bits.
|
# for backend bits.
|
||||||
@ -45,7 +40,28 @@ class DefaultConnectionProxy(object):
|
|||||||
return delattr(connections[DEFAULT_DB_ALIAS], name)
|
return delattr(connections[DEFAULT_DB_ALIAS], name)
|
||||||
|
|
||||||
connection = DefaultConnectionProxy()
|
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):
|
def close_connection(**kwargs):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
@ -390,9 +390,10 @@ class BaseDatabaseWrapper(object):
|
|||||||
def disable_constraint_checking(self):
|
def disable_constraint_checking(self):
|
||||||
"""
|
"""
|
||||||
Backends can implement as needed to temporarily disable foreign key
|
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):
|
def enable_constraint_checking(self):
|
||||||
"""
|
"""
|
||||||
@ -784,12 +785,12 @@ class BaseDatabaseOperations(object):
|
|||||||
"""
|
"""
|
||||||
return cursor.fetchone()[0]
|
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
|
Given a column type (e.g. 'BLOB', 'VARCHAR'), and an internal type
|
||||||
to cast it before using it in a WHERE statement. Note that the
|
(e.g. 'GenericIPAddressField'), returns the SQL necessary to cast it
|
||||||
resulting string should contain a '%s' placeholder for the column being
|
before using it in a WHERE statement. Note that the resulting string
|
||||||
searched against.
|
should contain a '%s' placeholder for the column being searched against.
|
||||||
"""
|
"""
|
||||||
return '%s'
|
return '%s'
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ class BaseDatabaseCreation(object):
|
|||||||
style.SQL_TABLE(qn(opts.db_table)) + ' (']
|
style.SQL_TABLE(qn(opts.db_table)) + ' (']
|
||||||
for i, line in enumerate(table_output): # Combine and add commas.
|
for i, line in enumerate(table_output): # Combine and add commas.
|
||||||
full_statement.append(
|
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(')')
|
full_statement.append(')')
|
||||||
if opts.db_tablespace:
|
if opts.db_tablespace:
|
||||||
tablespace_sql = self.connection.ops.tablespace_sql(
|
tablespace_sql = self.connection.ops.tablespace_sql(
|
||||||
|
@ -17,8 +17,7 @@ class SQLCompiler(compiler.SQLCompiler):
|
|||||||
values.append(value)
|
values.append(value)
|
||||||
return row[:index_extra_select] + tuple(values)
|
return row[:index_extra_select] + tuple(values)
|
||||||
|
|
||||||
def as_subquery_condition(self, alias, columns):
|
def as_subquery_condition(self, alias, columns, qn):
|
||||||
qn = self.quote_name_unless_alias
|
|
||||||
qn2 = self.connection.ops.quote_name
|
qn2 = self.connection.ops.quote_name
|
||||||
sql, params = self.as_sql()
|
sql, params = self.as_sql()
|
||||||
return '(%s) IN (%s)' % (', '.join(['%s.%s' % (qn(alias), qn2(column)) for column in columns]), sql), params
|
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
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e)
|
raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pytz
|
||||||
|
except ImportError:
|
||||||
|
pytz = None
|
||||||
|
|
||||||
from django.db import utils
|
from django.db import utils
|
||||||
from django.db.backends import *
|
from django.db.backends import *
|
||||||
from django.db.backends.oracle.client import DatabaseClient
|
from django.db.backends.oracle.client import DatabaseClient
|
||||||
@ -78,6 +83,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||||||
supports_subqueries_in_group_by = False
|
supports_subqueries_in_group_by = False
|
||||||
supports_transactions = True
|
supports_transactions = True
|
||||||
supports_timezones = False
|
supports_timezones = False
|
||||||
|
has_zoneinfo_database = pytz is not None
|
||||||
supports_bitwise_or = False
|
supports_bitwise_or = False
|
||||||
can_defer_constraint_checks = True
|
can_defer_constraint_checks = True
|
||||||
ignores_nulls_in_unique_constraints = False
|
ignores_nulls_in_unique_constraints = False
|
||||||
@ -243,9 +249,6 @@ WHEN (new.%(col_name)s IS NULL)
|
|||||||
value = value.date()
|
value = value.date()
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def datetime_cast_sql(self):
|
|
||||||
return "TO_TIMESTAMP(%s, 'YYYY-MM-DD HH24:MI:SS.FF')"
|
|
||||||
|
|
||||||
def deferrable_sql(self):
|
def deferrable_sql(self):
|
||||||
return " DEFERRABLE INITIALLY DEFERRED"
|
return " DEFERRABLE INITIALLY DEFERRED"
|
||||||
|
|
||||||
@ -255,7 +258,7 @@ WHEN (new.%(col_name)s IS NULL)
|
|||||||
def fetch_returned_insert_id(self, cursor):
|
def fetch_returned_insert_id(self, cursor):
|
||||||
return int(cursor._insert_id_var.getvalue())
|
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'):
|
if db_type and db_type.endswith('LOB'):
|
||||||
return "DBMS_LOB.SUBSTR(%s)"
|
return "DBMS_LOB.SUBSTR(%s)"
|
||||||
else:
|
else:
|
||||||
@ -434,6 +437,17 @@ WHEN (new.%(col_name)s IS NULL)
|
|||||||
second = '%s-12-31'
|
second = '%s-12-31'
|
||||||
return [first % value, second % value]
|
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):
|
def combine_expression(self, connector, sub_expressions):
|
||||||
"Oracle requires special cases for %% and & operators in query expressions"
|
"Oracle requires special cases for %% and & operators in query expressions"
|
||||||
if connector == '%%':
|
if connector == '%%':
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user