From d19109fd37e75ccf29d2ca64370102753dbc7c5b Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Fri, 21 Dec 2012 21:59:06 -0300 Subject: Fixed #19497 -- Refactored testing docs. Thanks Tim Graham for the review and suggestions. --- docs/internals/contributing/writing-code/unit-tests.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 4e702ff83e..afef554a8c 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -15,8 +15,8 @@ The tests cover: We appreciate any and all contributions to the test suite! The Django tests all use the testing infrastructure that ships with Django for -testing applications. See :doc:`Testing Django applications ` -for an explanation of how to write new tests. +testing applications. See :doc:`Testing Django applications +` for an explanation of how to write new tests. .. _running-unit-tests: -- cgit v1.3 From 9d62220e008c5e1b03e3026aaa97afac2e4eb67b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 22 Dec 2012 19:01:55 +0100 Subject: Fixed #15516 -- Updated the ticket life cycle diagram. --- docs/internals/_images/djangotickets.png | Bin 52003 -> 0 bytes docs/internals/_images/triage_process.graffle | 2652 ++++++++++++++++++++++ docs/internals/_images/triage_process.pdf | Bin 0 -> 70123 bytes docs/internals/_images/triage_process.svg | 3 + docs/internals/contributing/triaging-tickets.txt | 6 +- 5 files changed, 2658 insertions(+), 3 deletions(-) delete mode 100644 docs/internals/_images/djangotickets.png create mode 100644 docs/internals/_images/triage_process.graffle create mode 100644 docs/internals/_images/triage_process.pdf create mode 100644 docs/internals/_images/triage_process.svg (limited to 'docs/internals') diff --git a/docs/internals/_images/djangotickets.png b/docs/internals/_images/djangotickets.png deleted file mode 100644 index 34a2a41852..0000000000 Binary files a/docs/internals/_images/djangotickets.png and /dev/null differ diff --git a/docs/internals/_images/triage_process.graffle b/docs/internals/_images/triage_process.graffle new file mode 100644 index 0000000000..cd1e89cc3a --- /dev/null +++ b/docs/internals/_images/triage_process.graffle @@ -0,0 +1,2652 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGrafflePro + 139.16.0.171715 + + AutoAdjust + + BackgroundGraphic + + Bounds + {{0, 0}, {1118.5799560546875, 782.8900146484375}} + Class + SolidGraphic + ID + 2 + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + + BaseZoom + 0 + CanvasOrigin + {0, 0} + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2012-12-22 15:48:38 +0000 + Creator + Aymeric Augustin + DisplayScale + 1.000 cm = 1.000 cm + GraphDocumentVersion + 8 + GraphicsList + + + Class + LineGraphic + ID + 104 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {98.499995506345428, 441} + {45, 441} + {36, 576} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 103 + + + + Bounds + {{99, 432}, {18, 18}} + Class + ShapedGraphic + ID + 103 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Bounds + {{27, 576}, {342, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 102 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is.} + + VFlip + YES + + + Bounds + {{27, 543.5}, {342, 12}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + ID + 100 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs20 \cf0 For clarity, only the most common transitions are shown.} + VerticalPad + 0 + + + + Class + LineGraphic + ID + 98 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 + Points + + {98.499995506345428, 333} + {45, 333} + {36, 189} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + LineType + 2 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 97 + + + + Bounds + {{99, 324}, {18, 18}} + Class + ShapedGraphic + ID + 97 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Bounds + {{27, 135}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 96 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket is a bug and obviously should be fixed.} + + VFlip + YES + + + Bounds + {{189, 306}, {18, 18}} + Class + ShapedGraphic + ID + 94 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Class + LineGraphic + ID + 93 + Points + + {204.18279336665475, 307.78674107223611} + {252, 252} + {252, 189} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 94 + + + + Bounds + {{162, 135}, {180, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 95 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket requires a discussion by the community and a design decision by a core developer.} + + VFlip + YES + + + Bounds + {{387, 279}, {18, 18}} + Class + ShapedGraphic + ID + 91 + Shape + Circle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + + + Class + LineGraphic + ID + 90 + Points + + {396, 278.49999548261451} + {396, 189} + + Style + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + HeadArrow + 0 + Legacy + + LineType + 1 + Pattern + 1 + TailArrow + 0 + + + Tail + + ID + 91 + + + + Bounds + {{369, 135}, {198, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + HFlip + YES + ID + 89 + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Color + + b + 0.6 + g + 0.6 + r + 0.6 + + Pattern + 1 + + + Text + + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket was already reported, isn't a bug, doesn't provide enough information, or can't be reproduced.} + + VFlip + YES + + + Class + LineGraphic + Head + + ID + 132 + Info + 4 + + ID + 134 + Points + + {342, 342} + {393, 395} + {450, 450} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 16 + + + + Class + LineGraphic + Head + + ID + 132 + + ID + 133 + Points + + {342, 450} + {450, 450} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 17 + + + + Class + LineGraphic + Head + + ID + 10 + + ID + 60 + Points + + {108, 423} + {108, 477} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 11 + + + + Class + LineGraphic + ID + 82 + Points + + {162, 288} + {396, 288} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + 0 + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 12 + Info + 3 + + + + Class + LineGraphic + Head + + ID + 11 + + ID + 54 + Points + + {108, 315} + {108, 369} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 12 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 130 + + ID + 131 + Points + + {162, 504} + {450, 504} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 10 + Info + 3 + + + + Class + LineGraphic + Head + + ID + 11 + + ID + 58 + Points + + {234.0000000000002, 342} + {162, 396} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 16 + + + + Class + LineGraphic + Head + + ID + 11 + + ID + 57 + Points + + {234.0000000000002, 450} + {162, 396} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 17 + + + + Class + LineGraphic + Head + + ID + 17 + + ID + 56 + Points + + {288, 369} + {288, 423} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 16 + + + + Class + LineGraphic + Head + + ID + 16 + + ID + 55 + Points + + {162, 288} + {234.0000000000002, 342} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 12 + + + + Class + LineGraphic + Head + + ID + 135 + Info + 4 + + ID + 136 + Points + + {396, 288} + {450, 405} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 137 + + ID + 138 + Points + + {396, 288} + {450, 360} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 139 + + ID + 140 + Points + + {396, 288} + {450, 315} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Class + LineGraphic + Head + + ID + 123 + Info + 4 + + ID + 124 + Points + + {396, 288} + {450, 270} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + TailArrow + 0 + Width + 2 + + + Tail + + ID + 82 + Info + 1 + + + + Bounds + {{315, 630}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 128 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 development status} + + + + Bounds + {{26.999999999999993, 650}, {108.00000000000001, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 45 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red0\green64\blue128;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr + +\f0\b\fs24 \cf2 Committers} + VerticalPad + 0 + + + + Class + LineGraphic + ID + 44 + Points + + {144, 657} + {180, 657} + + Style + + stroke + + Color + + b + 0.501961 + g + 0.25098 + r + 0 + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 2 + + + + + Bounds + {{26.999999999999993, 632}, {108.00000000000001, 14}} + Class + ShapedGraphic + FitText + Vertical + Flow + Resize + FontInfo + + Font + Helvetica + Size + 12 + + ID + 43 + Shape + Rectangle + Style + + fill + + Draws + NO + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Align + 2 + Pad + 0 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red0\green128\blue0;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr + +\f0\b\fs24 \cf2 Ticket triagers } + VerticalPad + 0 + + + + Class + LineGraphic + ID + 42 + Points + + {144, 639} + {180, 639} + + Style + + stroke + + Color + + b + 0 + g + 0.501961 + r + 0 + + HeadArrow + FilledArrow + Legacy + + LineType + 1 + TailArrow + 0 + Width + 2 + + + + + Bounds + {{315, 648}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 129 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 in progress} + + + + Bounds + {{441, 630}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 125 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 stopped} + + + + Bounds + {{441, 648}, {125.99999999999999, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 127 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0.501961 + r + 0 + + GradientCenter + {0, 0.15238095234285712} + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 completed} + + + + Class + LineGraphic + ID + 36 + Points + + {423, 234} + {567, 234} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + Class + LineGraphic + ID + 33 + Points + + {27, 234} + {369, 234} + + Style + + stroke + + HeadArrow + 0 + Legacy + + TailArrow + 0 + + + + + Bounds + {{450, 441}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 132 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 wontfix} + + + + Bounds + {{450, 396}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 135 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 worksforme} + + + + Bounds + {{450, 351}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 137 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 needsinfo} + + + + Bounds + {{450, 306}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 139 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 invalid} + + + + Bounds + {{450, 495}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 130 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0.501961 + r + 0 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 fixed} + + + + Bounds + {{450, 261}, {90.000000000000014, 18}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 123 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 0 + g + 0 + r + 1 + + GradientCenter + {0, 0.15238095234285712} + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 duplicate} + + + + Bounds + {{234, 423}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 17 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Someday\ +/\ +Mabye} + + + + Bounds + {{234, 315}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 16 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Design\ +Decision\ +Needed} + + + + Bounds + {{54, 261}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 12 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Unreviewed} + + + + Bounds + {{54, 369}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 11 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Accepted} + + + + Bounds + {{54, 477}, {108, 54}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 10 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + Color + + a + 0.3 + b + 1 + g + 0.501961 + r + 0 + + + stroke + + CornerRadius + 5 + + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs24 \cf0 Ready for Checkin} + + + + Bounds + {{27, 207}, {342, 351}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 13 + + ID + 99 + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs28 \cf0 Open tickets\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs12 \cf0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs24 \cf0 triage state} + + TextPlacement + 0 + + + Bounds + {{423, 207}, {144, 351}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 15 + + ID + 32 + Shape + Rectangle + Style + + Text + + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\fs28 \cf0 Closed tickets\ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs12 \cf0 \ +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\fs24 \cf0 resolution} + + TextPlacement + 0 + + + Bounds + {{315, 630}, {252, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica-Bold + Size + 12 + + ID + 126 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + GradientCenter + {0, 0.15238095234285712} + + stroke + + Draws + NO + + + + + Bounds + {{27, 630}, {180, 36}} + Class + ShapedGraphic + FontInfo + + Font + Helvetica + Size + 12 + + ID + 88 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + Rectangle + Style + + fill + + GradientCenter + {0, 0.15238095234285712} + + stroke + + Draws + NO + + + + + GridInfo + + ShowsGrid + YES + SnapsToGrid + YES + + GuidesLocked + NO + GuidesVisible + YES + HPages + 2 + ImageCounter + 1 + KeepToScale + + Layers + + + Lock + NO + Name + Calque 1 + Print + YES + View + YES + + + LayoutInfo + + Animate + NO + circoMinDist + 18 + circoSeparation + 0.0 + layoutEngine + dot + neatoSeparation + 0.0 + twopiSeparation + 0.0 + + LinksVisible + NO + MagnetsVisible + NO + MasterSheets + + ModificationDate + 2012-12-22 18:00:58 +0000 + Modifier + Aymeric Augustin + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 41 + + NSHorizonalPagination + + coded + BAtzdHJlYW10eXBlZIHoA4QBQISEhAhOU051bWJlcgCEhAdOU1ZhbHVlAISECE5TT2JqZWN0AIWEASqEhAFxlwCG + + NSLeftMargin + + float + 18 + + NSPaperSize + + size + {595.28997802734375, 841.8900146484375} + + NSPrintReverseOrientation + + int + 0 + + NSRightMargin + + float + 18 + + NSTopMargin + + float + 18 + + + PrintOnePage + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canevas 1 + SmartAlignmentGuidesActive + YES + SmartDistanceGuidesActive + YES + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + ExpandedCanvases + + Frame + {{1, 4}, {1190, 874}} + ListView + + OutlineWidth + 142 + RightSidebar + + ShowRuler + + Sidebar + + SidebarWidth + 120 + VisibleRegion + {{0, 50.450449800270746}, {950.45043820152921, 662.1621536285536}} + Zoom + 1.1100000143051147 + ZoomValues + + + Canevas 1 + 1.1100000143051147 + 1.0499999523162842 + + + + + diff --git a/docs/internals/_images/triage_process.pdf b/docs/internals/_images/triage_process.pdf new file mode 100644 index 0000000000..a157fa8960 Binary files /dev/null and b/docs/internals/_images/triage_process.pdf differ diff --git a/docs/internals/_images/triage_process.svg b/docs/internals/_images/triage_process.svg new file mode 100644 index 0000000000..363ba41aef --- /dev/null +++ b/docs/internals/_images/triage_process.svg @@ -0,0 +1,3 @@ + + +2012-12-22 18:00ZCanevas 1Calque 1Closed ticketsresolutionOpen ticketstriage stateReady for CheckinAcceptedUnreviewedDesignDecisionNeededSomeday/Mabyeduplicatefixedinvalidneedsinfoworksformewontfixcompletedstoppedin progressTicket triagers Committersdevelopment statusThe ticket was already reported, isn't a bug, doesn't provide enough information, or can't be reproduced.The ticket requires a discussion by the community and a design decision by a core developer.The ticket is a bug and obviously should be fixed.For clarity, only the most common transitions are shown.The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is. diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index 84f70fd731..19298c55fb 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -50,9 +50,9 @@ attribute easily tells us what and who each ticket is waiting on. Since a picture is worth a thousand words, let's start there: -.. image:: /internals/_images/djangotickets.png - :height: 451 - :width: 590 +.. image:: /internals/_images/triage_process.* + :height: 564 + :width: 580 :alt: Django's ticket triage workflow We've got two roles in this diagram: -- cgit v1.3 From 35d1cd0b28d1d9cd7bffbfbc6cc2e02b58404415 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sat, 22 Dec 2012 20:00:08 +0100 Subject: Fixed #19505 -- A more flexible implementation for customizable admin redirect urls. Work by Julien Phalip. Refs #8001, #18310, #19505. See also 0b908b92a2ca4fb74a103e96bb75c53c05d0a428. --- django/contrib/admin/options.py | 167 ++++++++-------------- django/contrib/auth/admin.py | 5 +- docs/internals/deprecation.txt | 6 + tests/regressiontests/admin_custom_urls/models.py | 51 ++++--- tests/regressiontests/admin_custom_urls/tests.py | 81 ++++++----- 5 files changed, 140 insertions(+), 170 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 1827d40159..fa6d288f58 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -9,7 +9,7 @@ from django.forms.models import (modelform_factory, modelformset_factory, inlineformset_factory, BaseInlineFormSet) from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets, helpers -from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects, model_format_dict +from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_format_dict from django.contrib.admin.templatetags.admin_static import static from django.contrib import messages from django.views.decorators.csrf import csrf_protect @@ -38,6 +38,7 @@ HORIZONTAL, VERTICAL = 1, 2 # returns the
    class for a given radio_admin field get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '') + class IncorrectLookupParameters(Exception): pass @@ -62,6 +63,7 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = { csrf_protect_m = method_decorator(csrf_protect) + class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): """Functionality common to both ModelAdmin and InlineAdmin.""" @@ -150,7 +152,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): }) if 'choices' not in kwargs: kwargs['choices'] = db_field.get_choices( - include_blank = db_field.blank, + include_blank=db_field.blank, blank_choice=[('', _('None'))] ) return db_field.formfield(**kwargs) @@ -787,49 +789,37 @@ class ModelAdmin(BaseModelAdmin): "admin/change_form.html" ], context, current_app=self.admin_site.name) - def response_add(self, request, obj, post_url_continue='../%s/', - continue_editing_url=None, add_another_url=None, - hasperm_url=None, noperm_url=None): + def response_add(self, request, obj, post_url_continue=None): """ Determines the HttpResponse for the add_view stage. - - :param request: HttpRequest instance. - :param obj: Object just added. - :param post_url_continue: Deprecated/undocumented. - :param continue_editing_url: URL where user will be redirected after - pressing 'Save and continue editing'. - :param add_another_url: URL where user will be redirected after - pressing 'Save and add another'. - :param hasperm_url: URL to redirect after a successful object creation - when the user has change permissions. - :param noperm_url: URL to redirect after a successful object creation - when the user has no change permissions. - """ - if post_url_continue != '../%s/': - warnings.warn("The undocumented 'post_url_continue' argument to " - "ModelAdmin.response_add() is deprecated, use the new " - "*_url arguments instead.", DeprecationWarning, - stacklevel=2) + """ opts = obj._meta - pk_value = obj.pk - app_label = opts.app_label - model_name = opts.module_name - site_name = self.admin_site.name + pk_value = obj._get_pk_val() msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} - # Here, we distinguish between different save types by checking for # the presence of keys in request.POST. if "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict self.message_user(request, msg) - if continue_editing_url is None: - continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name) - url = reverse(continue_editing_url, args=(quote(pk_value),), - current_app=site_name) + if post_url_continue is None: + post_url_continue = reverse('admin:%s_%s_change' % + (opts.app_label, opts.module_name), + args=(pk_value,), + current_app=self.admin_site.name) + else: + try: + post_url_continue = post_url_continue % pk_value + warnings.warn( + "The use of string formats for post_url_continue " + "in ModelAdmin.response_add() is deprecated. Provide " + "a pre-formatted url instead.", + DeprecationWarning, stacklevel=2) + except TypeError: + pass if "_popup" in request.POST: - url += "?_popup=1" - return HttpResponseRedirect(url) + post_url_continue += "?_popup=1" + return HttpResponseRedirect(post_url_continue) if "_popup" in request.POST: return HttpResponse( @@ -840,102 +830,61 @@ class ModelAdmin(BaseModelAdmin): elif "_addanother" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may add another %(name)s below.') % msg_dict self.message_user(request, msg) - if add_another_url is None: - add_another_url = 'admin:%s_%s_add' % (app_label, model_name) - url = reverse(add_another_url, current_app=site_name) - return HttpResponseRedirect(url) + return HttpResponseRedirect(request.path) else: msg = _('The %(name)s "%(obj)s" was added successfully.') % msg_dict self.message_user(request, msg) + return self.response_post_save(request, obj) - # Figure out where to redirect. If the user has change permission, - # redirect to the change-list page for this object. Otherwise, - # redirect to the admin index. - if self.has_change_permission(request, None): - if hasperm_url is None: - hasperm_url = 'admin:%s_%s_changelist' % (app_label, model_name) - url = reverse(hasperm_url, current_app=site_name) - else: - if noperm_url is None: - noperm_url = 'admin:index' - url = reverse(noperm_url, current_app=site_name) - return HttpResponseRedirect(url) - - def response_change(self, request, obj, continue_editing_url=None, - save_as_new_url=None, add_another_url=None, - hasperm_url=None, noperm_url=None): + def response_change(self, request, obj): """ Determines the HttpResponse for the change_view stage. - - :param request: HttpRequest instance. - :param obj: Object just modified. - :param continue_editing_url: URL where user will be redirected after - pressing 'Save and continue editing'. - :param save_as_new_url: URL where user will be redirected after pressing - 'Save as new' (when applicable). - :param add_another_url: URL where user will be redirected after pressing - 'Save and add another'. - :param hasperm_url: URL to redirect after a successful object edition when - the user has change permissions. - :param noperm_url: URL to redirect after a successful object edition when - the user has no change permissions. """ - opts = obj._meta + opts = self.model._meta - app_label = opts.app_label - model_name = opts.module_name - site_name = self.admin_site.name - verbose_name = opts.verbose_name - # Handle proxy models automatically created by .only() or .defer(). - # Refs #14529 - if obj._deferred: - opts_ = opts.proxy_for_model._meta - verbose_name = opts_.verbose_name - model_name = opts_.module_name - - msg_dict = {'name': force_text(verbose_name), 'obj': force_text(obj)} + pk_value = obj._get_pk_val() + msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} if "_continue" in request.POST: msg = _('The %(name)s "%(obj)s" was changed successfully. You may edit it again below.') % msg_dict self.message_user(request, msg) - if continue_editing_url is None: - continue_editing_url = 'admin:%s_%s_change' % (app_label, model_name) - url = reverse(continue_editing_url, args=(quote(obj.pk),), - current_app=site_name) - if "_popup" in request.POST: - url += "?_popup=1" - return HttpResponseRedirect(url) + if "_popup" in request.REQUEST: + return HttpResponseRedirect(request.path + "?_popup=1") + else: + return HttpResponseRedirect(request.path) elif "_saveasnew" in request.POST: msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict self.message_user(request, msg) - if save_as_new_url is None: - save_as_new_url = 'admin:%s_%s_change' % (app_label, model_name) - url = reverse(save_as_new_url, args=(quote(obj.pk),), - current_app=site_name) - return HttpResponseRedirect(url) + return HttpResponseRedirect(reverse('admin:%s_%s_change' % + (opts.app_label, opts.module_name), + args=(pk_value,), + current_app=self.admin_site.name)) elif "_addanother" in request.POST: msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict self.message_user(request, msg) - if add_another_url is None: - add_another_url = 'admin:%s_%s_add' % (app_label, model_name) - url = reverse(add_another_url, current_app=site_name) - return HttpResponseRedirect(url) + return HttpResponseRedirect(reverse('admin:%s_%s_add' % + (opts.app_label, opts.module_name), + current_app=self.admin_site.name)) else: msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict self.message_user(request, msg) - # Figure out where to redirect. If the user has change permission, - # redirect to the change-list page for this object. Otherwise, - # redirect to the admin index. - if self.has_change_permission(request, None): - if hasperm_url is None: - hasperm_url = 'admin:%s_%s_changelist' % (app_label, - model_name) - url = reverse(hasperm_url, current_app=site_name) - else: - if noperm_url is None: - noperm_url = 'admin:index' - url = reverse(noperm_url, current_app=site_name) - return HttpResponseRedirect(url) + return self.response_post_save(request, obj) + + def response_post_save(self, request, obj): + """ + Figure out where to redirect after the 'Save' button has been pressed. + If the user has change permission, redirect to the change-list page for + this object. Otherwise, redirect to the admin index. + """ + opts = self.model._meta + if self.has_change_permission(request, None): + post_url = reverse('admin:%s_%s_changelist' % + (opts.app_label, opts.module_name), + current_app=self.admin_site.name) + else: + post_url = reverse('admin:index', + current_app=self.admin_site.name) + return HttpResponseRedirect(post_url) def response_action(self, request, queryset): """ diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index d15a387a7e..7b816674d3 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -153,7 +153,7 @@ class UserAdmin(admin.ModelAdmin): 'admin/auth/user/change_password.html', context, current_app=self.admin_site.name) - def response_add(self, request, obj, **kwargs): + def response_add(self, request, obj, post_url_continue=None): """ Determines the HttpResponse for the add_view stage. It mostly defers to its superclass implementation but is customized because the User model @@ -166,7 +166,8 @@ class UserAdmin(admin.ModelAdmin): # * We are adding a user in a popup if '_addanother' not in request.POST and '_popup' not in request.POST: request.POST['_continue'] = 1 - return super(UserAdmin, self).response_add(request, obj, **kwargs) + return super(UserAdmin, self).response_add(request, obj, + post_url_continue) admin.site.register(Group, GroupAdmin) admin.site.register(User, UserAdmin) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 414da30ff8..386dfc0b00 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -268,6 +268,12 @@ these changes. * ``django.contrib.markup`` will be removed following an accelerated deprecation. +* The value for the ``post_url_continue`` parameter in + ``ModelAdmin.response_add()`` will have to be either ``None`` (to redirect + to the newly created object's edit page) or a pre-formatted url. String + formats, such as the previous default ``'../%s/'``, will not be accepted any + more. + 1.7 --- diff --git a/tests/regressiontests/admin_custom_urls/models.py b/tests/regressiontests/admin_custom_urls/models.py index b9b3285463..cc8e730c26 100644 --- a/tests/regressiontests/admin_custom_urls/models.py +++ b/tests/regressiontests/admin_custom_urls/models.py @@ -1,7 +1,9 @@ from functools import update_wrapper from django.contrib import admin +from django.core.urlresolvers import reverse from django.db import models +from django.http import HttpResponseRedirect from django.utils.encoding import python_2_unicode_compatible @@ -49,41 +51,38 @@ class ActionAdmin(admin.ModelAdmin): ) + self.remove_url(view_name) -admin.site.register(Action, ActionAdmin) +class Person(models.Model): + name = models.CharField(max_length=20) +class PersonAdmin(admin.ModelAdmin): -class Person(models.Model): - nick = models.CharField(max_length=20) + def response_post_save(self, request, obj): + return HttpResponseRedirect( + reverse('admin:admin_custom_urls_person_history', args=[obj.pk])) -class PersonAdmin(admin.ModelAdmin): - """A custom ModelAdmin that customizes the deprecated post_url_continue - argument to response_add()""" - def response_add(self, request, obj, post_url_continue='../%s/continue/', - continue_url=None, add_url=None, hasperm_url=None, - noperm_url=None): - return super(PersonAdmin, self).response_add(request, obj, - post_url_continue, - continue_url, add_url, - hasperm_url, noperm_url) +class Car(models.Model): + name = models.CharField(max_length=20) +class CarAdmin(admin.ModelAdmin): -admin.site.register(Person, PersonAdmin) + def response_add(self, request, obj, post_url_continue=None): + return super(CarAdmin, self).response_add( + request, obj, post_url_continue=reverse('admin:admin_custom_urls_car_history', args=[obj.pk])) -class City(models.Model): +class CarDeprecated(models.Model): + """ This class must be removed in Django 1.6 """ name = models.CharField(max_length=20) - -class CityAdmin(admin.ModelAdmin): - """A custom ModelAdmin that redirects to the changelist when the user - presses the 'Save and add another' button when adding a model instance.""" - def response_add(self, request, obj, - add_another_url='admin:admin_custom_urls_city_changelist', - **kwargs): - return super(CityAdmin, self).response_add(request, obj, - add_another_url=add_another_url, - **kwargs) +class CarDeprecatedAdmin(admin.ModelAdmin): + """ This class must be removed in Django 1.6 """ + def response_add(self, request, obj, post_url_continue=None): + return super(CarDeprecatedAdmin, self).response_add( + request, obj, post_url_continue='../%s/history/') -admin.site.register(City, CityAdmin) +admin.site.register(Action, ActionAdmin) +admin.site.register(Person, PersonAdmin) +admin.site.register(Car, CarAdmin) +admin.site.register(CarDeprecated, CarDeprecatedAdmin) \ No newline at end of file diff --git a/tests/regressiontests/admin_custom_urls/tests.py b/tests/regressiontests/admin_custom_urls/tests.py index 87c72e2e71..d691a97557 100644 --- a/tests/regressiontests/admin_custom_urls/tests.py +++ b/tests/regressiontests/admin_custom_urls/tests.py @@ -1,5 +1,4 @@ from __future__ import absolute_import, unicode_literals - import warnings from django.contrib.admin.util import quote @@ -8,7 +7,7 @@ from django.template.response import TemplateResponse from django.test import TestCase from django.test.utils import override_settings -from .models import Action, Person, City +from .models import Action, Person, Car, CarDeprecated @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) @@ -86,8 +85,8 @@ class AdminCustomUrlsTest(TestCase): @override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',)) -class CustomUrlsWorkflowTests(TestCase): - fixtures = ['users.json'] +class CustomRedirects(TestCase): + fixtures = ['users.json', 'actions.json'] def setUp(self): self.client.login(username='super', password='secret') @@ -95,33 +94,49 @@ class CustomUrlsWorkflowTests(TestCase): def tearDown(self): self.client.logout() - def test_old_argument_deprecation(self): - """Test reporting of post_url_continue deprecation.""" - post_data = { - 'nick': 'johndoe', - } - cnt = Person.objects.count() - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - response = self.client.post(reverse('admin:admin_custom_urls_person_add'), post_data) - self.assertEqual(response.status_code, 302) - self.assertEqual(Person.objects.count(), cnt + 1) - # We should get a DeprecationWarning - self.assertEqual(len(w), 1) - self.assertTrue(isinstance(w[0].message, DeprecationWarning)) - - def test_custom_add_another_redirect(self): - """Test customizability of post-object-creation redirect URL.""" - post_data = { - 'name': 'Rome', - '_addanother': '1', - } - cnt = City.objects.count() + def test_post_save_redirect(self): + """ + Ensures that ModelAdmin.response_post_save() controls the redirection + after the 'Save' button has been pressed. + Refs 8001, 18310, 19505. + """ + post_data = { 'name': 'John Doe', } + self.assertEqual(Person.objects.count(), 0) + response = self.client.post( + reverse('admin:admin_custom_urls_person_add'), post_data) + persons = Person.objects.all() + self.assertEqual(len(persons), 1) + self.assertRedirects( + response, reverse('admin:admin_custom_urls_person_history', args=[persons[0].pk])) + + def test_post_url_continue(self): + """ + Ensures that the ModelAdmin.response_add()'s parameter `post_url_continue` + controls the redirection after an object has been created. + """ + post_data = { 'name': 'SuperFast', '_continue': '1' } + self.assertEqual(Car.objects.count(), 0) + response = self.client.post( + reverse('admin:admin_custom_urls_car_add'), post_data) + cars = Car.objects.all() + self.assertEqual(len(cars), 1) + self.assertRedirects( + response, reverse('admin:admin_custom_urls_car_history', args=[cars[0].pk])) + + def test_post_url_continue_string_formats(self): + """ + Ensures that string formats are accepted for post_url_continue. This + is a deprecated functionality that will be removed in Django 1.6 along + with this test. + """ with warnings.catch_warnings(record=True) as w: - # POST to the view whose post-object-creation redir URL argument we - # are customizing (object creation) - response = self.client.post(reverse('admin:admin_custom_urls_city_add'), post_data) - self.assertEqual(City.objects.count(), cnt + 1) - # Check that it redirected to the URL we set - self.assertRedirects(response, reverse('admin:admin_custom_urls_city_changelist')) - self.assertEqual(len(w), 0) # We should get no DeprecationWarning + post_data = { 'name': 'SuperFast', '_continue': '1' } + self.assertEqual(Car.objects.count(), 0) + response = self.client.post( + reverse('admin:admin_custom_urls_cardeprecated_add'), post_data) + cars = CarDeprecated.objects.all() + self.assertEqual(len(cars), 1) + self.assertRedirects( + response, reverse('admin:admin_custom_urls_cardeprecated_history', args=[cars[0].pk])) + self.assertEqual(len(w), 1) + self.assertTrue(isinstance(w[0].message, DeprecationWarning)) \ No newline at end of file -- cgit v1.3 From 4500d3522defd7df869756e8ee2c876a747a8fa9 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 26 Dec 2012 14:19:28 +0100 Subject: Fixed #19518 -- Documented the deprecation of localflavor. Also moved the contrib deprecations at the top of their section and made minor markup fixes. --- docs/internals/deprecation.txt | 14 +++++++++----- docs/ref/contrib/localflavor.txt | 6 ++++++ docs/releases/1.5.txt | 31 ++++++++++++++++++++++--------- 3 files changed, 37 insertions(+), 14 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 386dfc0b00..77f03ae2c7 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -14,7 +14,7 @@ See the :doc:`Django 1.2 release notes` for more details on these changes. * ``CsrfResponseMiddleware`` and ``CsrfMiddleware`` will be removed. Use - the {% csrf_token %} template tag inside forms to enable CSRF + the ``{% csrf_token %}`` template tag inside forms to enable CSRF protection. ``CsrfViewMiddleware`` remains and is enabled by default. * The old imports for CSRF functionality (``django.contrib.csrf.*``), @@ -200,6 +200,14 @@ these changes. See the :doc:`Django 1.4 release notes` for more details on these changes. +* ``django.contrib.databrowse`` will be removed. + +* ``django.contrib.localflavor`` will be removed following an accelerated + deprecation. + +* ``django.contrib.markup`` will be removed following an accelerated + deprecation. + * The compatibility modules ``django.utils.copycompat`` and ``django.utils.hashcompat`` as well as the functions ``django.utils.itercompat.all`` and ``django.utils.itercompat.any`` will @@ -251,8 +259,6 @@ these changes. :data:`~django.conf.urls.handler500`, are now available through :mod:`django.conf.urls` . -* The Databrowse contrib module will be removed. - * The functions :func:`~django.core.management.setup_environ` and :func:`~django.core.management.execute_manager` will be removed from :mod:`django.core.management`. This also means that the old (pre-1.4) @@ -265,8 +271,6 @@ these changes. in 1.4. The backward compatibility will be removed -- ``HttpRequest.raw_post_data`` will no longer work. -* ``django.contrib.markup`` will be removed following an accelerated - deprecation. * The value for the ``post_url_continue`` parameter in ``ModelAdmin.response_add()`` will have to be either ``None`` (to redirect diff --git a/docs/ref/contrib/localflavor.txt b/docs/ref/contrib/localflavor.txt index 9bb27e6e74..84569feebe 100644 --- a/docs/ref/contrib/localflavor.txt +++ b/docs/ref/contrib/localflavor.txt @@ -37,6 +37,8 @@ file. .. _ISO 3166 country code: http://www.iso.org/iso/country_codes.htm +.. _localflavor-how-to-migrate: + How to migrate ============== @@ -60,6 +62,8 @@ The code in the new packages is the same (it was copied directly from Django), so you don't have to worry about backwards compatibility in terms of functionality. Only the imports have changed. +.. _localflavor-deprecation-policy: + Deprecation policy ================== @@ -70,6 +74,8 @@ change it as soon as possible. In Django 1.6, importing from ``django.contrib.localflavor`` will no longer work. +.. _localflavor-packages: + Supported countries =================== diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 6ac5120617..8259c12cb4 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -634,7 +634,26 @@ Miscellaneous Features deprecated in 1.5 ========================== -.. _simplejson-deprecation: +``django.contrib.localflavor`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The localflavor contrib app has been split into separate packages. +``django.contrib.localflavor`` itself will be removed in Django 1.6, after an +:ref:`accelerated deprecation `. The docs +provide :ref:`migration instructions `. + +The new packages are available :ref:`on Github `. The +core team cannot efficiently maintain these packages in the long term — it +spans just a dozen countries at this time; similar to translations, maintenance +will be handed over to interested members of the community. + +``django.contrib.markup`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The markup contrib module has been deprecated and will follow an accelerated +deprecation schedule. Direct use of python markup libraries or 3rd party tag +libraries is preferred to Django maintaining this functionality in the +framework. :setting:`AUTH_PROFILE_MODULE` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -660,6 +679,8 @@ to :class:`~django.http.HttpResponse`. If you rely on this behavior, switch to In Django 1.7 and above, the iterator will be consumed immediately by :class:`~django.http.HttpResponse`. +.. _simplejson-deprecation: + ``django.utils.simplejson`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -687,14 +708,6 @@ Define a ``__str__`` method and apply the The :func:`~django.utils.itercompat.product` function has been deprecated. Use the built-in :func:`itertools.product` instead. -``django.utils.markup`` -~~~~~~~~~~~~~~~~~~~~~~~ - -The markup contrib module has been deprecated and will follow an accelerated -deprecation schedule. Direct use of python markup libraries or 3rd party tag -libraries is preferred to Django maintaining this functionality in the -framework. - ``cleanup`` management command ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.3 From 11ded967c443087487f3872aafd86842608b4c64 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Fri, 28 Dec 2012 11:00:11 -0800 Subject: Fixed #19498 -- refactored auth documentation The auth doc was a single page which had grown unwieldy. This refactor split and grouped the content into sub-topics. Additional corrections and cleanups were made along the way. --- docs/howto/deployment/wsgi/apache-auth.txt | 2 +- docs/index.txt | 2 +- docs/internals/git.txt | 2 +- docs/misc/api-stability.txt | 2 +- docs/ref/authbackends.txt | 33 - docs/ref/contrib/auth.txt | 428 ++++- docs/ref/contrib/index.txt | 2 +- docs/ref/django-admin.txt | 4 +- docs/ref/index.txt | 1 - docs/ref/middleware.txt | 4 +- docs/ref/request-response.txt | 2 +- docs/ref/settings.txt | 6 +- docs/ref/signals.txt | 2 +- docs/releases/1.2-beta-1.txt | 4 +- docs/releases/1.2.txt | 4 +- docs/releases/1.3-beta-1.txt | 2 +- docs/releases/1.3.txt | 2 +- docs/topics/auth.txt | 2696 ---------------------------- docs/topics/auth/customizing.txt | 1074 +++++++++++ docs/topics/auth/default.txt | 1077 +++++++++++ docs/topics/auth/index.txt | 81 + docs/topics/auth/passwords.txt | 212 +++ docs/topics/db/models.txt | 58 +- docs/topics/index.txt | 2 +- docs/topics/testing/overview.txt | 4 +- 25 files changed, 2920 insertions(+), 2786 deletions(-) delete mode 100644 docs/ref/authbackends.txt delete mode 100644 docs/topics/auth.txt create mode 100644 docs/topics/auth/customizing.txt create mode 100644 docs/topics/auth/default.txt create mode 100644 docs/topics/auth/index.txt create mode 100644 docs/topics/auth/passwords.txt (limited to 'docs/internals') diff --git a/docs/howto/deployment/wsgi/apache-auth.txt b/docs/howto/deployment/wsgi/apache-auth.txt index 5f700f1cb3..220645947d 100644 --- a/docs/howto/deployment/wsgi/apache-auth.txt +++ b/docs/howto/deployment/wsgi/apache-auth.txt @@ -4,7 +4,7 @@ Authenticating against Django's user database from Apache Since keeping multiple authentication databases in sync is a common problem when dealing with Apache, you can configure Apache to authenticate against Django's -:doc:`authentication system ` directly. This requires Apache +:doc:`authentication system ` directly. This requires Apache version >= 2.2 and mod_wsgi >= 2.0. For example, you could: * Serve static/media files directly from Apache only to authenticated users. diff --git a/docs/index.txt b/docs/index.txt index e8e7eadb23..971c2ff479 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -246,7 +246,7 @@ Common Web application tools Django offers multiple tools commonly needed in the development of Web applications: -* :doc:`Authentication ` +* :doc:`Authentication ` * :doc:`Caching ` * :doc:`Logging ` * :doc:`Sending emails ` diff --git a/docs/internals/git.txt b/docs/internals/git.txt index 948f9a1f7e..2b1a279d89 100644 --- a/docs/internals/git.txt +++ b/docs/internals/git.txt @@ -134,7 +134,7 @@ part of Django itself, and so are no longer separately maintained: of Django since the 0.95 release. * ``multi-auth``: A refactoring of :doc:`Django's bundled - authentication framework ` which added support for + authentication framework ` which added support for :ref:`authentication backends `. This has been part of Django since the 0.95 release. diff --git a/docs/misc/api-stability.txt b/docs/misc/api-stability.txt index a13cb5de69..70e6006575 100644 --- a/docs/misc/api-stability.txt +++ b/docs/misc/api-stability.txt @@ -38,7 +38,7 @@ In general, everything covered in the documentation -- with the exception of anything in the :doc:`internals area ` is considered stable as of 1.0. This includes these APIs: -- :doc:`Authorization ` +- :doc:`Authorization ` - :doc:`Caching `. diff --git a/docs/ref/authbackends.txt b/docs/ref/authbackends.txt deleted file mode 100644 index 55a536e819..0000000000 --- a/docs/ref/authbackends.txt +++ /dev/null @@ -1,33 +0,0 @@ -======================= -Authentication backends -======================= - -.. module:: django.contrib.auth.backends - :synopsis: Django's built-in authentication backend classes. - -This document details the authentication backends that come with Django. For -information on how to use them and how to write your own authentication -backends, see the :ref:`Other authentication sources section -` of the :doc:`User authentication guide -`. - - -Available authentication backends -================================= - -The following backends are available in :mod:`django.contrib.auth.backends`: - -.. class:: ModelBackend - - This is the default authentication backend used by Django. It - authenticates using usernames and passwords stored in the - :class:`~django.contrib.auth.models.User` model. - - -.. class:: RemoteUserBackend - - Use this backend to take advantage of external-to-Django-handled - authentication. It authenticates using usernames passed in - :attr:`request.META['REMOTE_USER'] `. See - the :doc:`Authenticating against REMOTE_USER ` - documentation. diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 619b38e5ac..41f218b0a4 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -1,4 +1,430 @@ ``django.contrib.auth`` ======================= -See :doc:`/topics/auth`. +This document provides API reference material for the components of Django's +authentication system. For more details on the usage of these components or +how to customize authentication and authorization see the :doc:`authentication +topic guide `. + +.. currentmodule:: django.contrib.auth + +User +==== + +Fields +------ + +.. class:: models.User + + :class:`~django.contrib.auth.models.User` objects have the following + fields: + + .. attribute:: username + + Required. 30 characters or fewer. Usernames may contain alphanumeric, + ``_``, ``@``, ``+``, ``.`` and ``-`` characters. + + .. attribute:: first_name + + Optional. 30 characters or fewer. + + .. attribute:: last_name + + Optional. 30 characters or fewer. + + .. attribute:: email + + Optional. Email address. + + .. attribute:: password + + Required. A hash of, and metadata about, the password. (Django doesn't + store the raw password.) Raw passwords can be arbitrarily long and can + contain any character. See the :doc:`password documentation + `. + + .. attribute:: groups + + Many-to-many relationship to :class:`~django.contrib.auth.models.Group` + + .. attribute:: user_permissions + + Many-to-many relationship to :class:`~django.contrib.auth.models.Permission` + + .. attribute:: is_staff + + Boolean. Designates whether this user can access the admin site. + + .. attribute:: is_active + + Boolean. Designates whether this user account should be considered + active. We recommend that you set this flag to ``False`` instead of + deleting accounts; that way, if your applications have any foreign keys + to users, the foreign keys won't break. + + This doesn't necessarily control whether or not the user can log in. + Authentication backends aren't required to check for the ``is_active`` + flag, and the default backends do not. If you want to reject a login + based on ``is_active`` being ``False``, it's up to you to check that in + your own login view or a custom authentication backend. However, the + :class:`~django.contrib.auth.forms.AuthenticationForm` used by the + :func:`~django.contrib.auth.views.login` view (which is the default) + *does* perform this check, as do the permission-checking methods such + as :meth:`~django.contrib.auth.models.User.has_perm` and the + authentication in the Django admin. All of those functions/methods will + return ``False`` for inactive users. + + .. attribute:: is_superuser + + Boolean. Designates that this user has all permissions without + explicitly assigning them. + + .. attribute:: last_login + + A datetime of the user's last login. Is set to the current date/time by + default. + + .. attribute:: date_joined + + A datetime designating when the account was created. Is set to the + current date/time by default when the account is created. + +Methods +------- + +.. class:: models.User + + .. method:: get_username() + + Returns the username for the user. Since the User model can be swapped + out, you should use this method instead of referencing the username + attribute directly. + + .. method:: is_anonymous() + + Always returns ``False``. This is a way of differentiating + :class:`~django.contrib.auth.models.User` and + :class:`~django.contrib.auth.models.AnonymousUser` objects. + Generally, you should prefer using + :meth:`~django.contrib.auth.models.User.is_authenticated()` to this + method. + + .. method:: is_authenticated() + + Always returns ``True``. This is a way to tell if the user has been + authenticated. This does not imply any permissions, and doesn't check + if the user is active - it only indicates that the user has provided a + valid username and password. + + .. method:: get_full_name() + + Returns the :attr:`~django.contrib.auth.models.User.first_name` plus + the :attr:`~django.contrib.auth.models.User.last_name`, with a space in + between. + + .. method:: set_password(raw_password) + + Sets the user's password to the given raw string, taking care of the + password hashing. Doesn't save the + :class:`~django.contrib.auth.models.User` object. + + .. method:: check_password(raw_password) + + Returns ``True`` if the given raw string is the correct password for + the user. (This takes care of the password hashing in making the + comparison.) + + .. method:: set_unusable_password() + + Marks the user as having no password set. This isn't the same as + having a blank string for a password. + :meth:`~django.contrib.auth.models.User.check_password()` for this user + will never return ``True``. Doesn't save the + :class:`~django.contrib.auth.models.User` object. + + You may need this if authentication for your application takes place + against an existing external source such as an LDAP directory. + + .. method:: has_usable_password() + + Returns ``False`` if + :meth:`~django.contrib.auth.models.User.set_unusable_password()` has + been called for this user. + + .. method:: get_group_permissions(obj=None) + + Returns a set of permission strings that the user has, through his/her + groups. + + If ``obj`` is passed in, only returns the group permissions for + this specific object. + + .. method:: get_all_permissions(obj=None) + + Returns a set of permission strings that the user has, both through + group and user permissions. + + If ``obj`` is passed in, only returns the permissions for this + specific object. + + .. method:: has_perm(perm, obj=None) + + Returns ``True`` if the user has the specified permission, where perm + is in the format ``"."``. (see + documentation on :ref:`permissions `). If the user is + inactive, this method will always return ``False``. + + If ``obj`` is passed in, this method won't check for a permission for + the model, but for this specific object. + + .. method:: has_perms(perm_list, obj=None) + + Returns ``True`` if the user has each of the specified permissions, + where each perm is in the format + ``"."``. If the user is inactive, + this method will always return ``False``. + + If ``obj`` is passed in, this method won't check for permissions for + the model, but for the specific object. + + .. method:: has_module_perms(package_name) + + Returns ``True`` if the user has any permissions in the given package + (the Django app label). If the user is inactive, this method will + always return ``False``. + + .. method:: email_user(subject, message, from_email=None) + + Sends an email to the user. If ``from_email`` is ``None``, Django uses + the :setting:`DEFAULT_FROM_EMAIL`. + + .. method:: get_profile() + + .. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + + Returns a site-specific profile for this user. Raises + ``django.contrib.auth.models.SiteProfileNotAvailable`` if the + current site doesn't allow profiles, or + :exc:`django.core.exceptions.ObjectDoesNotExist` if the user does not + have a profile. + +Manager methods +--------------- + +.. class:: models.UserManager + + The :class:`~django.contrib.auth.models.User` model has a custom manager + that has the following helper methods: + + .. method:: create_user(username, email=None, password=None) + + .. versionchanged:: 1.4 + The ``email`` parameter was made optional. The username + parameter is now checked for emptiness and raises a + :exc:`~exceptions.ValueError` in case of a negative result. + + Creates, saves and returns a :class:`~django.contrib.auth.models.User`. + + The :attr:`~django.contrib.auth.models.User.username` and + :attr:`~django.contrib.auth.models.User.password` are set as given. The + domain portion of :attr:`~django.contrib.auth.models.User.email` is + automatically converted to lowercase, and the returned + :class:`~django.contrib.auth.models.User` object will have + :attr:`~django.contrib.auth.models.User.is_active` set to ``True``. + + If no password is provided, + :meth:`~django.contrib.auth.models.User.set_unusable_password()` will + be called. + + See :ref:`Creating users ` for example usage. + + .. method:: make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') + + Returns a random password with the given length and given string of + allowed characters. (Note that the default value of ``allowed_chars`` + doesn't contain letters that can cause user confusion, including: + + * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase + letter L, uppercase letter i, and the number one) + * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o, + and zero) + + +Anonymous users +=============== + +.. class:: models.AnonymousUser + + :class:`django.contrib.auth.models.AnonymousUser` is a class that + implements the :class:`django.contrib.auth.models.User` interface, with + these differences: + + * :ref:`id ` is always ``None``. + * :attr:`~django.contrib.auth.models.User.is_staff` and + :attr:`~django.contrib.auth.models.User.is_superuser` are always + ``False``. + * :attr:`~django.contrib.auth.models.User.is_active` is always ``False``. + * :attr:`~django.contrib.auth.models.User.groups` and + :attr:`~django.contrib.auth.models.User.user_permissions` are always + empty. + * :meth:`~django.contrib.auth.models.User.is_anonymous()` returns ``True`` + instead of ``False``. + * :meth:`~django.contrib.auth.models.User.is_authenticated()` returns + ``False`` instead of ``True``. + * :meth:`~django.contrib.auth.models.User.set_password()`, + :meth:`~django.contrib.auth.models.User.check_password()`, + :meth:`~django.db.models.Model.save` and + :meth:`~django.db.models.Model.delete()` raise + :exc:`~exceptions.NotImplementedError`. + +In practice, you probably won't need to use +:class:`~django.contrib.auth.models.AnonymousUser` objects on your own, but +they're used by Web requests, as explained in the next section. + +Permission +========== + +.. class:: models.Permission + +Fields +------ + +:class:`~django.contrib.auth.models.Permission` objects have the following +fields: + +.. attribute:: name + + Required. 50 characters or fewer. Example: ``'Can vote'``. + +.. attribute:: content_type + + Required. A reference to the ``django_content_type`` database table, which + contains a record for each installed Django model. + +.. attribute:: codename + + Required. 100 characters or fewer. Example: ``'can_vote'``. + +Methods +------- + +:class:`~django.contrib.auth.models.Permission` objects have the standard +data-access methods like any other :doc:`Django model `. + +Group +===== + +.. class:: models.Group + +Fields +------ + +:class:`~django.contrib.auth.models.Group` objects have the following fields: + +.. attribute:: name + + Required. 80 characters or fewer. Any characters are permitted. Example: + ``'Awesome Users'``. + +.. attribute:: permissions + + Many-to-many field to :class:`~django.contrib.auth.models.Permission`:: + + group.permissions = [permission_list] + group.permissions.add(permission, permission, ...) + group.permissions.remove(permission, permission, ...) + group.permissions.clear() + +.. _topics-auth-signals: + +Login and logout signals +======================== + +.. module:: django.contrib.auth.signals + +The auth framework uses two :doc:`signals ` that can be used +for notification when a user logs in or out. + +.. function:: django.contrib.auth.signals.user_logged_in + + Sent when a user logs in successfully. + + Arguments sent with this signal: + + ``sender`` + The class of the user that just logged in. + + ``request`` + The current :class:`~django.http.HttpRequest` instance. + + ``user`` + The user instance that just logged in. + +.. function:: django.contrib.auth.signals.user_logged_out + + Sent when the logout method is called. + + ``sender`` + As above: the class of the user that just logged out or ``None`` + if the user was not authenticated. + + ``request`` + The current :class:`~django.http.HttpRequest` instance. + + ``user`` + The user instance that just logged out or ``None`` if the + user was not authenticated. + +.. function:: django.contrib.auth.signals.user_login_failed + +.. versionadded:: 1.5 + + Sent when the user failed to login successfully + + ``sender`` + The name of the module used for authentication. + + ``credentials`` + A dictionary of keyword arguments containing the user credentials that were + passed to :func:`~django.contrib.auth.authenticate()` or your own custom + authentication backend. Credentials matching a set of 'sensitive' patterns, + (including password) will not be sent in the clear as part of the signal. + +.. _authentication-backends-reference: + +Authentication backends +======================= + +.. module:: django.contrib.auth.backends + :synopsis: Django's built-in authentication backend classes. + +This section details the authentication backends that come with Django. For +information on how to use them and how to write your own authentication +backends, see the :ref:`Other authentication sources section +` of the :doc:`User authentication guide +`. + + +Available authentication backends +--------------------------------- + +The following backends are available in :mod:`django.contrib.auth.backends`: + +.. class:: ModelBackend + + This is the default authentication backend used by Django. It + authenticates using usernames and passwords stored in the + :class:`~django.contrib.auth.models.User` model. + + +.. class:: RemoteUserBackend + + Use this backend to take advantage of external-to-Django-handled + authentication. It authenticates using usernames passed in + :attr:`request.META['REMOTE_USER'] `. See + the :doc:`Authenticating against REMOTE_USER ` + documentation. diff --git a/docs/ref/contrib/index.txt b/docs/ref/contrib/index.txt index efe4393f64..3bf5288ee4 100644 --- a/docs/ref/contrib/index.txt +++ b/docs/ref/contrib/index.txt @@ -56,7 +56,7 @@ auth Django's authentication framework. -See :doc:`/topics/auth`. +See :doc:`/topics/auth/index`. comments ======== diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index 6ab3b1d133..205f349e8b 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1145,7 +1145,7 @@ changepassword .. django-admin:: changepassword This command is only available if Django's :doc:`authentication system -` (``django.contrib.auth``) is installed. +` (``django.contrib.auth``) is installed. Allows changing a user's password. It prompts you to enter twice the password of the user given as parameter. If they both match, the new password will be @@ -1167,7 +1167,7 @@ createsuperuser .. django-admin:: createsuperuser This command is only available if Django's :doc:`authentication system -` (``django.contrib.auth``) is installed. +` (``django.contrib.auth``) is installed. Creates a superuser account (a user who has all permissions). This is useful if you need to create an initial superuser account but did not diff --git a/docs/ref/index.txt b/docs/ref/index.txt index e1959d44a6..fc874a97eb 100644 --- a/docs/ref/index.txt +++ b/docs/ref/index.txt @@ -5,7 +5,6 @@ API Reference .. toctree:: :maxdepth: 1 - authbackends class-based-views/index clickjacking contrib/index diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index b542aee6e2..41cff346ff 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -179,8 +179,8 @@ Authentication middleware .. class:: AuthenticationMiddleware Adds the ``user`` attribute, representing the currently-logged-in user, to -every incoming ``HttpRequest`` object. See :doc:`Authentication in Web requests -`. +every incoming ``HttpRequest`` object. See :ref:`Authentication in Web requests +`. CSRF protection middleware -------------------------- diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index c3ba99168d..2775c974d0 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -181,7 +181,7 @@ All attributes should be considered read-only, unless stated otherwise below. ``user`` is only available if your Django installation has the ``AuthenticationMiddleware`` activated. For more, see - :doc:`/topics/auth`. + :doc:`/topics/auth/index`. .. attribute:: HttpRequest.session diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 7cb33a038d..5815062266 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -107,8 +107,8 @@ AUTHENTICATION_BACKENDS Default: ``('django.contrib.auth.backends.ModelBackend',)`` A tuple of authentication backend classes (as strings) to use when attempting to -authenticate a user. See the :doc:`authentication backends documentation -` for details. +authenticate a user. See the :ref:`authentication backends documentation +` for details. .. setting:: AUTH_USER_MODEL @@ -2256,7 +2256,7 @@ AUTH_PROFILE_MODULE Default: Not defined The site-specific user profile model used by this site. See -:ref:`auth-profiles`. +:ref:`User profiles `. .. setting:: IGNORABLE_404_ENDS diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 2eefb0ca77..0671d80b7c 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -12,7 +12,7 @@ A list of all the signals that Django sends. The :doc:`comment framework ` sends a :doc:`set of comment-related signals `. - The :doc:`authentication framework ` sends :ref:`signals when + The :doc:`authentication framework ` sends :ref:`signals when a user is logged in / out `. Model signals diff --git a/docs/releases/1.2-beta-1.txt b/docs/releases/1.2-beta-1.txt index 99cd274957..3549767379 100644 --- a/docs/releases/1.2-beta-1.txt +++ b/docs/releases/1.2-beta-1.txt @@ -91,7 +91,7 @@ added in Django 1.2 alpha but not documented with the alpha release. The default authentication backends shipped with Django do not currently make use of this, but third-party authentication backends -are free to do so. See the :doc:`authentication docs ` +are free to do so. See the :doc:`authentication docs ` for more information. @@ -104,7 +104,7 @@ class will check the backend for permissions, just as the normal ``User`` does. This is intended to help centralize permission handling; apps can always delegate the question of whether something is allowed or not to the authorization/authentication system. See the -:doc:`authentication docs ` for more details. +:doc:`authentication docs ` for more details. ``select_related()`` improvements diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 26a9405595..68cec91587 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -165,7 +165,7 @@ A foundation for specifying permissions at the per-object level has been added. Although there is no implementation of this in core, a custom authentication backend can provide this implementation and it will be used by :class:`django.contrib.auth.models.User`. See the :doc:`authentication docs -` for more information. +` for more information. Permissions for anonymous users ------------------------------- @@ -175,7 +175,7 @@ If you provide a custom auth backend with ``supports_anonymous_user`` set to User already did. This is useful for centralizing permission handling - apps can always delegate the question of whether something is allowed or not to the authorization/authentication backend. See the :doc:`authentication -docs ` for more details. +docs ` for more details. Relaxed requirements for usernames ---------------------------------- diff --git a/docs/releases/1.3-beta-1.txt b/docs/releases/1.3-beta-1.txt index 02c038f459..2729c7f2ba 100644 --- a/docs/releases/1.3-beta-1.txt +++ b/docs/releases/1.3-beta-1.txt @@ -61,7 +61,7 @@ Permissions for inactive users If you provide a custom auth backend with ``supports_inactive_user`` set to ``True``, an inactive user model will check the backend for permissions. This is useful for further centralizing the permission handling. See the -:doc:`authentication docs ` for more details. +:doc:`authentication docs ` for more details. Backwards-incompatible changes in 1.3 alpha 2 ============================================= diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index c507f1bed6..d6ef11d113 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -254,7 +254,7 @@ Permissions for inactive users If you provide a custom auth backend with ``supports_inactive_user`` set to ``True``, an inactive ``User`` instance will check the backend for permissions. This is useful for further centralizing the -permission handling. See the :doc:`authentication docs ` +permission handling. See the :doc:`authentication docs ` for more details. GeoDjango diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt deleted file mode 100644 index e7a0ff114e..0000000000 --- a/docs/topics/auth.txt +++ /dev/null @@ -1,2696 +0,0 @@ -============================= -User authentication in Django -============================= - -.. module:: django.contrib.auth - :synopsis: Django's authentication framework. - -Django comes with a user authentication system. It handles user accounts, -groups, permissions and cookie-based user sessions. This document explains how -things work. - -Overview -======== - -The auth system consists of: - -* Users -* Permissions: Binary (yes/no) flags designating whether a user may perform - a certain task. -* Groups: A generic way of applying labels and permissions to more than one - user. - -Installation -============ - -Authentication support is bundled as a Django application in -``django.contrib.auth``. To install it, do the following: - -1. Put ``'django.contrib.auth'`` and ``'django.contrib.contenttypes'`` in - your :setting:`INSTALLED_APPS` setting. - (The :class:`~django.contrib.auth.models.Permission` model in - :mod:`django.contrib.auth` depends on :mod:`django.contrib.contenttypes`.) -2. Run the command ``manage.py syncdb``. - -Note that the default :file:`settings.py` file created by -:djadmin:`django-admin.py startproject ` includes -``'django.contrib.auth'`` and ``'django.contrib.contenttypes'`` in -:setting:`INSTALLED_APPS` for convenience. If your :setting:`INSTALLED_APPS` -already contains these apps, feel free to run :djadmin:`manage.py syncdb -` again; you can run that command as many times as you'd like, and each -time it'll only install what's needed. - -The :djadmin:`syncdb` command creates the necessary database tables, creates -permission objects for all installed apps that need 'em, and prompts you to -create a superuser account the first time you run it. - -Once you've taken those steps, that's it. - -Users -===== - -.. class:: models.User - -API reference -------------- - -Fields -~~~~~~ - -.. class:: models.User - - :class:`~django.contrib.auth.models.User` objects have the following - fields: - - .. attribute:: models.User.username - - Required. 30 characters or fewer. Usernames may contain alphanumeric, - ``_``, ``@``, ``+``, ``.`` and ``-`` characters. - - .. attribute:: models.User.first_name - - Optional. 30 characters or fewer. - - .. attribute:: models.User.last_name - - Optional. 30 characters or fewer. - - .. attribute:: models.User.email - - Optional. Email address. - - .. attribute:: models.User.password - - Required. A hash of, and metadata about, the password. (Django doesn't - store the raw password.) Raw passwords can be arbitrarily long and can - contain any character. See the "Passwords" section below. - - .. attribute:: models.User.is_staff - - Boolean. Designates whether this user can access the admin site. - - .. attribute:: models.User.is_active - - Boolean. Designates whether this user account should be considered - active. We recommend that you set this flag to ``False`` instead of - deleting accounts; that way, if your applications have any foreign keys - to users, the foreign keys won't break. - - This doesn't necessarily control whether or not the user can log in. - Authentication backends aren't required to check for the ``is_active`` - flag, and the default backends do not. If you want to reject a login - based on ``is_active`` being ``False``, it's up to you to check that in - your own login view or a custom authentication backend. However, the - :class:`~django.contrib.auth.forms.AuthenticationForm` used by the - :func:`~django.contrib.auth.views.login` view (which is the default) - *does* perform this check, as do the permission-checking methods such - as :meth:`~models.User.has_perm` and the authentication in the Django - admin. All of those functions/methods will return ``False`` for - inactive users. - - .. attribute:: models.User.is_superuser - - Boolean. Designates that this user has all permissions without - explicitly assigning them. - - .. attribute:: models.User.last_login - - A datetime of the user's last login. Is set to the current date/time by - default. - - .. attribute:: models.User.date_joined - - A datetime designating when the account was created. Is set to the - current date/time by default when the account is created. - -Methods -~~~~~~~ - -.. class:: models.User - - :class:`~django.contrib.auth.models.User` objects have two many-to-many - fields: ``groups`` and ``user_permissions``. - :class:`~django.contrib.auth.models.User` objects can access their related - objects in the same way as any other :doc:`Django model - `: - - .. code-block:: python - - myuser.groups = [group_list] - myuser.groups.add(group, group, ...) - myuser.groups.remove(group, group, ...) - myuser.groups.clear() - myuser.user_permissions = [permission_list] - myuser.user_permissions.add(permission, permission, ...) - myuser.user_permissions.remove(permission, permission, ...) - myuser.user_permissions.clear() - - In addition to those automatic API methods, - :class:`~django.contrib.auth.models.User` objects have the following custom - methods: - - .. method:: models.User.get_username() - - Returns the username for the user. Since the User model can be swapped - out, you should use this method instead of referencing the username - attribute directly. - - .. method:: models.User.is_anonymous() - - Always returns ``False``. This is a way of differentiating - :class:`~django.contrib.auth.models.User` and - :class:`~django.contrib.auth.models.AnonymousUser` objects. - Generally, you should prefer using - :meth:`~django.contrib.auth.models.User.is_authenticated()` to this - method. - - .. method:: models.User.is_authenticated() - - Always returns ``True``. This is a way to tell if the user has been - authenticated. This does not imply any permissions, and doesn't check - if the user is active - it only indicates that the user has provided a - valid username and password. - - .. method:: models.User.get_full_name() - - Returns the :attr:`~django.contrib.auth.models.User.first_name` plus - the :attr:`~django.contrib.auth.models.User.last_name`, with a space in - between. - - .. method:: models.User.set_password(raw_password) - - Sets the user's password to the given raw string, taking care of the - password hashing. Doesn't save the - :class:`~django.contrib.auth.models.User` object. - - .. method:: models.User.check_password(raw_password) - - Returns ``True`` if the given raw string is the correct password for - the user. (This takes care of the password hashing in making the - comparison.) - - .. method:: models.User.set_unusable_password() - - Marks the user as having no password set. This isn't the same as - having a blank string for a password. - :meth:`~django.contrib.auth.models.User.check_password()` for this user - will never return ``True``. Doesn't save the - :class:`~django.contrib.auth.models.User` object. - - You may need this if authentication for your application takes place - against an existing external source such as an LDAP directory. - - .. method:: models.User.has_usable_password() - - Returns ``False`` if - :meth:`~django.contrib.auth.models.User.set_unusable_password()` has - been called for this user. - - .. method:: models.User.get_group_permissions(obj=None) - - Returns a set of permission strings that the user has, through his/her - groups. - - If ``obj`` is passed in, only returns the group permissions for - this specific object. - - .. method:: models.User.get_all_permissions(obj=None) - - Returns a set of permission strings that the user has, both through - group and user permissions. - - If ``obj`` is passed in, only returns the permissions for this - specific object. - - .. method:: models.User.has_perm(perm, obj=None) - - Returns ``True`` if the user has the specified permission, where perm is - in the format ``"."``. (see - `permissions`_ section below). If the user is inactive, this method will - always return ``False``. - - If ``obj`` is passed in, this method won't check for a permission for - the model, but for this specific object. - - .. method:: models.User.has_perms(perm_list, obj=None) - - Returns ``True`` if the user has each of the specified permissions, - where each perm is in the format - ``"."``. If the user is inactive, - this method will always return ``False``. - - If ``obj`` is passed in, this method won't check for permissions for - the model, but for the specific object. - - .. method:: models.User.has_module_perms(package_name) - - Returns ``True`` if the user has any permissions in the given package - (the Django app label). If the user is inactive, this method will - always return ``False``. - - .. method:: models.User.email_user(subject, message, from_email=None) - - Sends an email to the user. If - :attr:`~django.contrib.auth.models.User.from_email` is ``None``, Django - uses the :setting:`DEFAULT_FROM_EMAIL`. - - .. method:: models.User.get_profile() - - .. deprecated:: 1.5 - With the introduction of :ref:`custom User models `, - the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile - model is no longer supported. See the - :doc:`Django 1.5 release notes` for more information. - - Returns a site-specific profile for this user. Raises - :exc:`django.contrib.auth.models.SiteProfileNotAvailable` if the - current site doesn't allow profiles, or - :exc:`django.core.exceptions.ObjectDoesNotExist` if the user does not - have a profile. For information on how to define a site-specific user - profile, see the section on `storing additional user information`_ below. - -.. _storing additional user information: #storing-additional-information-about-users - -Manager functions -~~~~~~~~~~~~~~~~~ - -.. class:: models.UserManager - - The :class:`~django.contrib.auth.models.User` model has a custom manager - that has the following helper functions: - - .. method:: models.UserManager.create_user(username, email=None, password=None) - - .. versionchanged:: 1.4 - The ``email`` parameter was made optional. The username - parameter is now checked for emptiness and raises a - :exc:`~exceptions.ValueError` in case of a negative result. - - Creates, saves and returns a :class:`~django.contrib.auth.models.User`. - - The :attr:`~django.contrib.auth.models.User.username` and - :attr:`~django.contrib.auth.models.User.password` are set as given. The - domain portion of :attr:`~django.contrib.auth.models.User.email` is - automatically converted to lowercase, and the returned - :class:`~django.contrib.auth.models.User` object will have - :attr:`~models.User.is_active` set to ``True``. - - If no password is provided, - :meth:`~django.contrib.auth.models.User.set_unusable_password()` will - be called. - - See `Creating users`_ for example usage. - - .. method:: models.UserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') - - Returns a random password with the given length and given string of - allowed characters. (Note that the default value of ``allowed_chars`` - doesn't contain letters that can cause user confusion, including: - - * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase - letter L, uppercase letter i, and the number one) - * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o, - and zero) - -Basic usage ------------ - -.. _topics-auth-creating-users: - -Creating users -~~~~~~~~~~~~~~ - -The most basic way to create users is to use the -:meth:`~django.contrib.auth.models.UserManager.create_user` helper function -that comes with Django:: - - >>> from django.contrib.auth.models import User - >>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword') - - # At this point, user is a User object that has already been saved - # to the database. You can continue to change its attributes - # if you want to change other fields. - >>> user.is_staff = True - >>> user.save() - -You can also create users using the Django admin site. Assuming you've enabled -the admin site and hooked it to the URL ``/admin/``, the "Add user" page is at -``/admin/auth/user/add/``. You should also see a link to "Users" in the "Auth" -section of the main admin index page. The "Add user" admin page is different -than standard admin pages in that it requires you to choose a username and -password before allowing you to edit the rest of the user's fields. - -Also note: if you want your own user account to be able to create users using -the Django admin site, you'll need to give yourself permission to add users -*and* change users (i.e., the "Add user" and "Change user" permissions). If -your account has permission to add users but not to change them, you won't be -able to add users. Why? Because if you have permission to add users, you have -the power to create superusers, which can then, in turn, change other users. So -Django requires add *and* change permissions as a slight security measure. - -Changing passwords -~~~~~~~~~~~~~~~~~~ - -:djadmin:`manage.py changepassword *username* ` offers a method -of changing a User's password from the command line. It prompts you to -change the password of a given user which you must enter twice. If -they both match, the new password will be changed immediately. If you -do not supply a user, the command will attempt to change the password -whose username matches the current user. - -You can also change a password programmatically, using -:meth:`~django.contrib.auth.models.User.set_password()`: - -.. code-block:: python - - >>> from django.contrib.auth.models import User - >>> u = User.objects.get(username__exact='john') - >>> u.set_password('new password') - >>> u.save() - -Don't set the :attr:`~django.contrib.auth.models.User.password` attribute -directly unless you know what you're doing. This is explained in the next -section. - -.. _auth_password_storage: - -How Django stores passwords ---------------------------- - -.. versionadded:: 1.4 - Django 1.4 introduces a new flexible password storage system and uses - PBKDF2 by default. Previous versions of Django used SHA1, and other - algorithms couldn't be chosen. - -The :attr:`~django.contrib.auth.models.User.password` attribute of a -:class:`~django.contrib.auth.models.User` object is a string in this format:: - - algorithm$hash - -That's a storage algorithm, and hash, separated by the dollar-sign -character. The algorithm is one of a number of one way hashing or password -storage algorithms Django can use; see below. The hash is the result of the one- -way function. - -By default, Django uses the PBKDF2_ algorithm with a SHA256 hash, a -password stretching mechanism recommended by NIST_. This should be -sufficient for most users: it's quite secure, requiring massive -amounts of computing time to break. - -However, depending on your requirements, you may choose a different -algorithm, or even use a custom algorithm to match your specific -security situation. Again, most users shouldn't need to do this -- if -you're not sure, you probably don't. If you do, please read on: - -Django chooses the an algorithm by consulting the :setting:`PASSWORD_HASHERS` -setting. This is a list of hashing algorithm classes that this Django -installation supports. The first entry in this list (that is, -``settings.PASSWORD_HASHERS[0]``) will be used to store passwords, and all the -other entries are valid hashers that can be used to check existing passwords. -This means that if you want to use a different algorithm, you'll need to modify -:setting:`PASSWORD_HASHERS` to list your preferred algorithm first in the list. - -The default for :setting:`PASSWORD_HASHERS` is:: - - PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', - 'django.contrib.auth.hashers.CryptPasswordHasher', - ) - -This means that Django will use PBKDF2_ to store all passwords, but will support -checking passwords stored with PBKDF2SHA1, bcrypt_, SHA1_, etc. The next few -sections describe a couple of common ways advanced users may want to modify this -setting. - -.. _bcrypt_usage: - -Using bcrypt with Django -~~~~~~~~~~~~~~~~~~~~~~~~ - -Bcrypt_ is a popular password storage algorithm that's specifically designed -for long-term password storage. It's not the default used by Django since it -requires the use of third-party libraries, but since many people may want to -use it Django supports bcrypt with minimal effort. - -To use Bcrypt as your default storage algorithm, do the following: - -1. Install the `py-bcrypt`_ library (probably by running ``sudo pip install - py-bcrypt``, or downloading the library and installing it with ``python - setup.py install``). - -2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptPasswordHasher`` - first. That is, in your settings file, you'd put:: - - PASSWORD_HASHERS = ( - 'django.contrib.auth.hashers.BCryptPasswordHasher', - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', - 'django.contrib.auth.hashers.CryptPasswordHasher', - ) - - (You need to keep the other entries in this list, or else Django won't - be able to upgrade passwords; see below). - -That's it -- now your Django install will use Bcrypt as the default storage -algorithm. - -.. admonition:: Other bcrypt implementations - - There are several other implementations that allow bcrypt to be - used with Django. Django's bcrypt support is NOT directly - compatible with these. To upgrade, you will need to modify the - hashes in your database to be in the form `bcrypt$(raw bcrypt - output)`. For example: - `bcrypt$$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy`. - -Increasing the work factor -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The PBKDF2 and bcrypt algorithms use a number of iterations or rounds of -hashing. This deliberately slows down attackers, making attacks against hashed -passwords harder. However, as computing power increases, the number of -iterations needs to be increased. We've chosen a reasonable default (and will -increase it with each release of Django), but you may wish to tune it up or -down, depending on your security needs and available processing power. To do so, -you'll subclass the appropriate algorithm and override the ``iterations`` -parameters. For example, to increase the number of iterations used by the -default PBKDF2 algorithm: - -1. Create a subclass of ``django.contrib.auth.hashers.PBKDF2PasswordHasher``:: - - from django.contrib.auth.hashers import PBKDF2PasswordHasher - - class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher): - """ - A subclass of PBKDF2PasswordHasher that uses 100 times more iterations. - """ - iterations = PBKDF2PasswordHasher.iterations * 100 - - Save this somewhere in your project. For example, you might put this in - a file like ``myproject/hashers.py``. - -2. Add your new hasher as the first entry in :setting:`PASSWORD_HASHERS`:: - - PASSWORD_HASHERS = ( - 'myproject.hashers.MyPBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.BCryptPasswordHasher', - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', - 'django.contrib.auth.hashers.CryptPasswordHasher', - ) - - -That's it -- now your Django install will use more iterations when it -stores passwords using PBKDF2. - -Password upgrading -~~~~~~~~~~~~~~~~~~ - -When users log in, if their passwords are stored with anything other than -the preferred algorithm, Django will automatically upgrade the algorithm -to the preferred one. This means that old installs of Django will get -automatically more secure as users log in, and it also means that you -can switch to new (and better) storage algorithms as they get invented. - -However, Django can only upgrade passwords that use algorithms mentioned in -:setting:`PASSWORD_HASHERS`, so as you upgrade to new systems you should make -sure never to *remove* entries from this list. If you do, users using un- -mentioned algorithms won't be able to upgrade. - -.. _sha1: http://en.wikipedia.org/wiki/SHA1 -.. _pbkdf2: http://en.wikipedia.org/wiki/PBKDF2 -.. _nist: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf -.. _bcrypt: http://en.wikipedia.org/wiki/Bcrypt -.. _py-bcrypt: http://pypi.python.org/pypi/py-bcrypt/ - -Anonymous users ---------------- - -.. class:: models.AnonymousUser - - :class:`django.contrib.auth.models.AnonymousUser` is a class that - implements the :class:`django.contrib.auth.models.User` interface, with - these differences: - - * :attr:`~django.contrib.auth.models.User.id` is always ``None``. - * :attr:`~django.contrib.auth.models.User.is_staff` and - :attr:`~django.contrib.auth.models.User.is_superuser` are always - ``False``. - * :attr:`~django.contrib.auth.models.User.is_active` is always ``False``. - * :attr:`~django.contrib.auth.models.User.groups` and - :attr:`~django.contrib.auth.models.User.user_permissions` are always - empty. - * :meth:`~django.contrib.auth.models.User.is_anonymous()` returns ``True`` - instead of ``False``. - * :meth:`~django.contrib.auth.models.User.is_authenticated()` returns - ``False`` instead of ``True``. - * :meth:`~django.contrib.auth.models.User.set_password()`, - :meth:`~django.contrib.auth.models.User.check_password()`, - :meth:`~django.contrib.auth.models.User.save()`, - :meth:`~django.contrib.auth.models.User.delete()`, - :meth:`~django.contrib.auth.models.User.set_groups()` and - :meth:`~django.contrib.auth.models.User.set_permissions()` raise - :exc:`~exceptions.NotImplementedError`. - -In practice, you probably won't need to use -:class:`~django.contrib.auth.models.AnonymousUser` objects on your own, but -they're used by Web requests, as explained in the next section. - -.. _topics-auth-creating-superusers: - -Creating superusers -------------------- - -:djadmin:`manage.py syncdb ` prompts you to create a superuser the -first time you run it after adding ``'django.contrib.auth'`` to your -:setting:`INSTALLED_APPS`. If you need to create a superuser at a later date, -you can use a command line utility:: - - manage.py createsuperuser --username=joe --email=joe@example.com - -You will be prompted for a password. After you enter one, the user will be -created immediately. If you leave off the :djadminopt:`--username` or the -:djadminopt:`--email` options, it will prompt you for those values. - -If you're using an older release of Django, the old way of creating a superuser -on the command line still works:: - - python /path/to/django/contrib/auth/create_superuser.py - -...where :file:`/path/to` is the path to the Django codebase on your -filesystem. The ``manage.py`` command is preferred because it figures out the -correct path and environment for you. - -.. _auth-profiles: - -Storing additional information about users ------------------------------------------- - -.. deprecated:: 1.5 - With the introduction of :ref:`custom User models `, - the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile - model is no longer supported. See the - :doc:`Django 1.5 release notes` for more information. - -If you'd like to store additional information related to your users, Django -provides a method to specify a site-specific related model -- termed a "user -profile" -- for this purpose. - -To make use of this feature, define a model with fields for the -additional information you'd like to store, or additional methods -you'd like to have available, and also add a -:class:`~django.db.models.Field.OneToOneField` named ``user`` from your model -to the :class:`~django.contrib.auth.models.User` model. This will ensure only -one instance of your model can be created for each -:class:`~django.contrib.auth.models.User`. For example:: - - from django.contrib.auth.models import User - - class UserProfile(models.Model): - # This field is required. - user = models.OneToOneField(User) - - # Other fields here - accepted_eula = models.BooleanField() - favorite_animal = models.CharField(max_length=20, default="Dragons.") - - -To indicate that this model is the user profile model for a given site, fill in -the setting :setting:`AUTH_PROFILE_MODULE` with a string consisting of the -following items, separated by a dot: - -1. The name of the application (case sensitive) in which the user - profile model is defined (in other words, the - name which was passed to :djadmin:`manage.py startapp ` to create - the application). - -2. The name of the model (not case sensitive) class. - -For example, if the profile model was a class named ``UserProfile`` and was -defined inside an application named ``accounts``, the appropriate setting would -be:: - - AUTH_PROFILE_MODULE = 'accounts.UserProfile' - -When a user profile model has been defined and specified in this manner, each -:class:`~django.contrib.auth.models.User` object will have a method -- -:class:`~django.contrib.auth.models.User.get_profile()` -- which returns the -instance of the user profile model associated with that -:class:`~django.contrib.auth.models.User`. - -The method :class:`~django.contrib.auth.models.User.get_profile()` -does not create a profile if one does not exist. You need to register a handler -for the User model's :attr:`django.db.models.signals.post_save` signal and, in -the handler, if ``created`` is ``True``, create the associated user profile:: - - # in models.py - - from django.contrib.auth.models import User - from django.db.models.signals import post_save - - # definition of UserProfile from above - # ... - - def create_user_profile(sender, instance, created, **kwargs): - if created: - UserProfile.objects.create(user=instance) - - post_save.connect(create_user_profile, sender=User) - -.. seealso:: :doc:`/topics/signals` for more information on Django's signal - dispatcher. - -Adding UserProfile fields to the admin --------------------------------------- - -To add the UserProfile fields to the user page in the admin, define an -:class:`~django.contrib.admin.InlineModelAdmin` (for this example, we'll use a -:class:`~django.contrib.admin.StackedInline`) in your app's ``admin.py`` and -add it to a ``UserAdmin`` class which is registered with the -:class:`~django.contrib.auth.models.User` class:: - - from django.contrib import admin - from django.contrib.auth.admin import UserAdmin - from django.contrib.auth.models import User - - from my_user_profile_app.models import UserProfile - - # Define an inline admin descriptor for UserProfile model - # which acts a bit like a singleton - class UserProfileInline(admin.StackedInline): - model = UserProfile - can_delete = False - verbose_name_plural = 'profile' - - # Define a new User admin - class UserAdmin(UserAdmin): - inlines = (UserProfileInline, ) - - # Re-register UserAdmin - admin.site.unregister(User) - admin.site.register(User, UserAdmin) - -Authentication in Web requests -============================== - -Until now, this document has dealt with the low-level APIs for manipulating -authentication-related objects. On a higher level, Django can hook this -authentication framework into its system of -:class:`request objects `. - -First, install the -:class:`~django.contrib.sessions.middleware.SessionMiddleware` and -:class:`~django.contrib.auth.middleware.AuthenticationMiddleware` -middlewares by adding them to your :setting:`MIDDLEWARE_CLASSES` setting. See -the :doc:`session documentation ` for more information. - -Once you have those middlewares installed, you'll be able to access -:attr:`request.user ` in views. -:attr:`request.user ` will give you a -:class:`~django.contrib.auth.models.User` object representing the currently -logged-in user. If a user isn't currently logged in, -:attr:`request.user ` will be set to an instance -of :class:`~django.contrib.auth.models.AnonymousUser` (see the previous -section). You can tell them apart with -:meth:`~django.contrib.auth.models.User.is_authenticated()`, like so:: - - if request.user.is_authenticated(): - # Do something for authenticated users. - else: - # Do something for anonymous users. - -.. _how-to-log-a-user-in: - -How to log a user in --------------------- - -Django provides two functions in :mod:`django.contrib.auth`: -:func:`~django.contrib.auth.authenticate()` and -:func:`~django.contrib.auth.login()`. - -.. function:: authenticate() - - To authenticate a given username and password, use - :func:`~django.contrib.auth.authenticate()`. It takes two keyword - arguments, ``username`` and ``password``, and it returns a - :class:`~django.contrib.auth.models.User` object if the password is valid - for the given username. If the password is invalid, - :func:`~django.contrib.auth.authenticate()` returns ``None``. Example:: - - from django.contrib.auth import authenticate - user = authenticate(username='john', password='secret') - if user is not None: - if user.is_active: - print("You provided a correct username and password!") - else: - print("Your account has been disabled!") - else: - print("Your username and password were incorrect.") - -.. function:: login() - - To log a user in, in a view, use :func:`~django.contrib.auth.login()`. It - takes an :class:`~django.http.HttpRequest` object and a - :class:`~django.contrib.auth.models.User` object. - :func:`~django.contrib.auth.login()` saves the user's ID in the session, - using Django's session framework, so, as mentioned above, you'll need to - make sure to have the session middleware installed. - - Note that data set during the anonymous session is retained when the user - logs in. - - This example shows how you might use both - :func:`~django.contrib.auth.authenticate()` and - :func:`~django.contrib.auth.login()`:: - - from django.contrib.auth import authenticate, login - - def my_view(request): - username = request.POST['username'] - password = request.POST['password'] - user = authenticate(username=username, password=password) - if user is not None: - if user.is_active: - login(request, user) - # Redirect to a success page. - else: - # Return a 'disabled account' error message - else: - # Return an 'invalid login' error message. - -.. admonition:: Calling ``authenticate()`` first - - When you're manually logging a user in, you *must* call - :func:`~django.contrib.auth.authenticate()` before you call - :func:`~django.contrib.auth.login()`. - :func:`~django.contrib.auth.authenticate()` - sets an attribute on the :class:`~django.contrib.auth.models.User` noting - which authentication backend successfully authenticated that user (see the - `backends documentation`_ for details), and this information is needed - later during the login process. - -.. _backends documentation: #other-authentication-sources - -Manually managing a user's password ------------------------------------ - -.. currentmodule:: django.contrib.auth.hashers - -.. versionadded:: 1.4 - The :mod:`django.contrib.auth.hashers` module provides a set of functions - to create and validate hashed password. You can use them independently - from the ``User`` model. - -.. function:: check_password(password, encoded) - - .. versionadded:: 1.4 - - If you'd like to manually authenticate a user by comparing a plain-text - password to the hashed password in the database, use the convenience - function :func:`django.contrib.auth.hashers.check_password`. It takes two - arguments: the plain-text password to check, and the full value of a - user's ``password`` field in the database to check against, and returns - ``True`` if they match, ``False`` otherwise. - -.. function:: make_password(password[, salt, hashers]) - - .. versionadded:: 1.4 - - Creates a hashed password in the format used by this application. It takes - one mandatory argument: the password in plain-text. Optionally, you can - provide a salt and a hashing algorithm to use, if you don't want to use the - defaults (first entry of ``PASSWORD_HASHERS`` setting). - Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``, - ``'bcrypt'`` (see :ref:`bcrypt_usage`), ``'sha1'``, ``'md5'``, - ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` - if you have the ``crypt`` library installed. If the password argument is - ``None``, an unusable password is returned (a one that will be never - accepted by :func:`django.contrib.auth.hashers.check_password`). - -.. function:: is_password_usable(encoded_password) - - .. versionadded:: 1.4 - - Checks if the given string is a hashed password that has a chance - of being verified against :func:`django.contrib.auth.hashers.check_password`. - - -How to log a user out ---------------------- - -.. currentmodule:: django.contrib.auth - -.. function:: logout() - - To log out a user who has been logged in via - :func:`django.contrib.auth.login()`, use - :func:`django.contrib.auth.logout()` within your view. It takes an - :class:`~django.http.HttpRequest` object and has no return value. - Example:: - - from django.contrib.auth import logout - - def logout_view(request): - logout(request) - # Redirect to a success page. - - Note that :func:`~django.contrib.auth.logout()` doesn't throw any errors if - the user wasn't logged in. - - When you call :func:`~django.contrib.auth.logout()`, the session data for - the current request is completely cleaned out. All existing data is - removed. This is to prevent another person from using the same Web browser - to log in and have access to the previous user's session data. If you want - to put anything into the session that will be available to the user - immediately after logging out, do that *after* calling - :func:`django.contrib.auth.logout()`. - -.. _topics-auth-signals: - -Login and logout signals ------------------------- - -The auth framework uses two :doc:`signals ` that can be used -for notification when a user logs in or out. - -.. data:: django.contrib.auth.signals.user_logged_in - :module: - -Sent when a user logs in successfully. - -Arguments sent with this signal: - -``sender`` - The class of the user that just logged in. - -``request`` - The current :class:`~django.http.HttpRequest` instance. - -``user`` - The user instance that just logged in. - -.. data:: django.contrib.auth.signals.user_logged_out - :module: - -Sent when the logout method is called. - -``sender`` - As above: the class of the user that just logged out or ``None`` - if the user was not authenticated. - -``request`` - The current :class:`~django.http.HttpRequest` instance. - -``user`` - The user instance that just logged out or ``None`` if the - user was not authenticated. - -.. data:: django.contrib.auth.signals.user_login_failed - :module: -.. versionadded:: 1.5 - -Sent when the user failed to login successfully - -``sender`` - The name of the module used for authentication. - -``credentials`` - A dictonary of keyword arguments containing the user credentials that were - passed to :func:`~django.contrib.auth.authenticate()` or your own custom - authentication backend. Credentials matching a set of 'sensitive' patterns, - (including password) will not be sent in the clear as part of the signal. - -Limiting access to logged-in users ----------------------------------- - -The raw way -~~~~~~~~~~~ - -The simple, raw way to limit access to pages is to check -:meth:`request.user.is_authenticated() -` and either redirect to a -login page:: - - from django.http import HttpResponseRedirect - - def my_view(request): - if not request.user.is_authenticated(): - return HttpResponseRedirect('/login/?next=%s' % request.path) - # ... - -...or display an error message:: - - def my_view(request): - if not request.user.is_authenticated(): - return render_to_response('myapp/login_error.html') - # ... - -The login_required decorator -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. function:: decorators.login_required([redirect_field_name=REDIRECT_FIELD_NAME, login_url=None]) - - As a shortcut, you can use the convenient - :func:`~django.contrib.auth.decorators.login_required` decorator:: - - from django.contrib.auth.decorators import login_required - - @login_required - def my_view(request): - ... - - :func:`~django.contrib.auth.decorators.login_required` does the following: - - * If the user isn't logged in, redirect to - :setting:`settings.LOGIN_URL `, passing the current absolute - path in the query string. Example: ``/accounts/login/?next=/polls/3/``. - - * If the user is logged in, execute the view normally. The view code is - free to assume the user is logged in. - - By default, the path that the user should be redirected to upon - successful authentication is stored in a query string parameter called - ``"next"``. If you would prefer to use a different name for this parameter, - :func:`~django.contrib.auth.decorators.login_required` takes an - optional ``redirect_field_name`` parameter:: - - from django.contrib.auth.decorators import login_required - - @login_required(redirect_field_name='my_redirect_field') - def my_view(request): - ... - - Note that if you provide a value to ``redirect_field_name``, you will most - likely need to customize your login template as well, since the template - context variable which stores the redirect path will use the value of - ``redirect_field_name`` as its key rather than ``"next"`` (the default). - - :func:`~django.contrib.auth.decorators.login_required` also takes an - optional ``login_url`` parameter. Example:: - - from django.contrib.auth.decorators import login_required - - @login_required(login_url='/accounts/login/') - def my_view(request): - ... - - Note that if you don't specify the ``login_url`` parameter, you'll need to map - the appropriate Django view to :setting:`settings.LOGIN_URL `. For - example, using the defaults, add the following line to your URLconf:: - - (r'^accounts/login/$', 'django.contrib.auth.views.login'), - - .. versionchanged:: 1.5 - - As of version 1.5 :setting:`settings.LOGIN_URL ` now also accepts - view function names and :ref:`named URL patterns `. - This allows you to freely remap your login view within your URLconf - without having to update the setting. - -.. function:: views.login(request, [template_name, redirect_field_name, authentication_form]) - - **URL name:** ``login`` - - See :doc:`the URL documentation ` for details on using - named URL patterns. - - Here's what ``django.contrib.auth.views.login`` does: - - * If called via ``GET``, it displays a login form that POSTs to the - same URL. More on this in a bit. - - * If called via ``POST``, it tries to log the user in. If login is - successful, the view redirects to the URL specified in ``next``. If - ``next`` isn't provided, it redirects to - :setting:`settings.LOGIN_REDIRECT_URL ` (which - defaults to ``/accounts/profile/``). If login isn't successful, it - redisplays the login form. - - It's your responsibility to provide the login form in a template called - ``registration/login.html`` by default. This template gets passed four - template context variables: - - * ``form``: A :class:`~django.forms.Form` object representing the login - form. See the :doc:`forms documentation ` for - more on ``Form`` objects. - - * ``next``: The URL to redirect to after successful login. This may - contain a query string, too. - - * ``site``: The current :class:`~django.contrib.sites.models.Site`, - according to the :setting:`SITE_ID` setting. If you don't have the - site framework installed, this will be set to an instance of - :class:`~django.contrib.sites.models.RequestSite`, which derives the - site name and domain from the current - :class:`~django.http.HttpRequest`. - - * ``site_name``: An alias for ``site.name``. If you don't have the site - framework installed, this will be set to the value of - :attr:`request.META['SERVER_NAME'] `. - For more on sites, see :doc:`/ref/contrib/sites`. - - If you'd prefer not to call the template :file:`registration/login.html`, - you can pass the ``template_name`` parameter via the extra arguments to - the view in your URLconf. For example, this URLconf line would use - :file:`myapp/login.html` instead:: - - (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'myapp/login.html'}), - - You can also specify the name of the ``GET`` field which contains the URL - to redirect to after login by passing ``redirect_field_name`` to the view. - By default, the field is called ``next``. - - Here's a sample :file:`registration/login.html` template you can use as a - starting point. It assumes you have a :file:`base.html` template that - defines a ``content`` block: - - .. code-block:: html+django - - {% extends "base.html" %} - - {% block content %} - - {% if form.errors %} -

    Your username and password didn't match. Please try again.

    - {% endif %} - -
    - {% csrf_token %} - - - - - - - - - -
    {{ form.username.label_tag }}{{ form.username }}
    {{ form.password.label_tag }}{{ form.password }}
    - - - -
    - - {% endblock %} - - If you are using alternate authentication (see - :ref:`authentication-backends`) you can pass a custom authentication form - to the login view via the ``authentication_form`` parameter. This form must - accept a ``request`` keyword argument in its ``__init__`` method, and - provide a ``get_user`` method which returns the authenticated user object - (this method is only ever called after successful form validation). - - .. _forms documentation: ../forms/ - .. _site framework docs: ../sites/ - - .. versionadded:: 1.4 - - The :func:`~views.login` view and the :ref:`other-built-in-views` now all - return a :class:`~django.template.response.TemplateResponse` instance, - which allows you to easily customize the response data before rendering. - For more details, see the - :doc:`TemplateResponse documentation `. - -.. _other-built-in-views: - -Other built-in views --------------------- - -.. module:: django.contrib.auth.views - -In addition to the :func:`~views.login` view, the authentication system -includes a few other useful built-in views located in -:mod:`django.contrib.auth.views`: - -.. function:: logout(request, [next_page, template_name, redirect_field_name]) - - Logs a user out. - - **URL name:** ``logout`` - - See :doc:`the URL documentation ` for details on using - named URL patterns. - - **Optional arguments:** - - * ``next_page``: The URL to redirect to after logout. - - * ``template_name``: The full name of a template to display after - logging the user out. Defaults to - :file:`registration/logged_out.html` if no argument is supplied. - - * ``redirect_field_name``: The name of a ``GET`` field containing the - URL to redirect to after log out. Overrides ``next_page`` if the given - ``GET`` parameter is passed. - - **Template context:** - - * ``title``: The string "Logged out", localized. - - * ``site``: The current :class:`~django.contrib.sites.models.Site`, - according to the :setting:`SITE_ID` setting. If you don't have the - site framework installed, this will be set to an instance of - :class:`~django.contrib.sites.models.RequestSite`, which derives the - site name and domain from the current - :class:`~django.http.HttpRequest`. - - * ``site_name``: An alias for ``site.name``. If you don't have the site - framework installed, this will be set to the value of - :attr:`request.META['SERVER_NAME'] `. - For more on sites, see :doc:`/ref/contrib/sites`. - -.. function:: logout_then_login(request[, login_url]) - - Logs a user out, then redirects to the login page. - - **URL name:** No default URL provided - - **Optional arguments:** - - * ``login_url``: The URL of the login page to redirect to. - Defaults to :setting:`settings.LOGIN_URL ` if not supplied. - -.. function:: password_change(request[, template_name, post_change_redirect, password_change_form]) - - Allows a user to change their password. - - **URL name:** ``password_change`` - - **Optional arguments:** - - * ``template_name``: The full name of a template to use for - displaying the password change form. Defaults to - :file:`registration/password_change_form.html` if not supplied. - - * ``post_change_redirect``: The URL to redirect to after a successful - password change. - - * ``password_change_form``: A custom "change password" form which must - accept a ``user`` keyword argument. The form is responsible for - actually changing the user's password. Defaults to - :class:`~django.contrib.auth.forms.PasswordChangeForm`. - - **Template context:** - - * ``form``: The password change form (see ``password_change_form`` above). - -.. function:: password_change_done(request[, template_name]) - - The page shown after a user has changed their password. - - **URL name:** ``password_change_done`` - - **Optional arguments:** - - * ``template_name``: The full name of a template to use. - Defaults to :file:`registration/password_change_done.html` if not - supplied. - -.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email]) - - Allows a user to reset their password by generating a one-time use link - that can be used to reset the password, and sending that link to the - user's registered email address. - - .. versionchanged:: 1.4 - Users flagged with an unusable password (see - :meth:`~django.contrib.auth.models.User.set_unusable_password()` - will not be able to request a password reset to prevent misuse - when using an external authentication source like LDAP. - - **URL name:** ``password_reset`` - - **Optional arguments:** - - * ``template_name``: The full name of a template to use for - displaying the password reset form. Defaults to - :file:`registration/password_reset_form.html` if not supplied. - - * ``email_template_name``: The full name of a template to use for - generating the email with the reset password link. Defaults to - :file:`registration/password_reset_email.html` if not supplied. - - * ``subject_template_name``: The full name of a template to use for - the subject of the email with the reset password link. Defaults - to :file:`registration/password_reset_subject.txt` if not supplied. - - .. versionadded:: 1.4 - - * ``password_reset_form``: Form that will be used to get the email of - the user to reset the password for. Defaults to - :class:`~django.contrib.auth.forms.PasswordResetForm`. - - * ``token_generator``: Instance of the class to check the one time link. - This will default to ``default_token_generator``, it's an instance of - ``django.contrib.auth.tokens.PasswordResetTokenGenerator``. - - * ``post_reset_redirect``: The URL to redirect to after a successful - password reset request. - - * ``from_email``: A valid email address. By default Django uses - the :setting:`DEFAULT_FROM_EMAIL`. - - **Template context:** - - * ``form``: The form (see ``password_reset_form`` above) for resetting - the user's password. - - **Email template context:** - - * ``email``: An alias for ``user.email`` - - * ``user``: The current :class:`~django.contrib.auth.models.User`, - according to the ``email`` form field. Only active users are able to - reset their passwords (``User.is_active is True``). - - * ``site_name``: An alias for ``site.name``. If you don't have the site - framework installed, this will be set to the value of - :attr:`request.META['SERVER_NAME'] `. - For more on sites, see :doc:`/ref/contrib/sites`. - - * ``domain``: An alias for ``site.domain``. If you don't have the site - framework installed, this will be set to the value of - ``request.get_host()``. - - * ``protocol``: http or https - - * ``uid``: The user's id encoded in base 36. - - * ``token``: Token to check that the reset link is valid. - - Sample ``registration/password_reset_email.html`` (email body template): - - .. code-block:: html+django - - Someone asked for password reset for email {{ email }}. Follow the link below: - {{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb36=uid token=token %} - - The same template context is used for subject template. Subject must be - single line plain text string. - - -.. function:: password_reset_done(request[, template_name]) - - The page shown after a user has been emailed a link to reset their - password. This view is called by default if the :func:`password_reset` view - doesn't have an explicit ``post_reset_redirect`` URL set. - - **URL name:** ``password_reset_done`` - - **Optional arguments:** - - * ``template_name``: The full name of a template to use. - Defaults to :file:`registration/password_reset_done.html` if not - supplied. - -.. function:: password_reset_confirm(request[, uidb36, token, template_name, token_generator, set_password_form, post_reset_redirect]) - - Presents a form for entering a new password. - - **URL name:** ``password_reset_confirm`` - - **Optional arguments:** - - * ``uidb36``: The user's id encoded in base 36. Defaults to ``None``. - - * ``token``: Token to check that the password is valid. Defaults to - ``None``. - - * ``template_name``: The full name of a template to display the confirm - password view. Default value is :file:`registration/password_reset_confirm.html`. - - * ``token_generator``: Instance of the class to check the password. This - will default to ``default_token_generator``, it's an instance of - ``django.contrib.auth.tokens.PasswordResetTokenGenerator``. - - * ``set_password_form``: Form that will be used to set the password. - Defaults to :class:`~django.contrib.auth.forms.SetPasswordForm` - - * ``post_reset_redirect``: URL to redirect after the password reset - done. Defaults to ``None``. - - **Template context:** - - * ``form``: The form (see ``set_password_form`` above) for setting the - new user's password. - - * ``validlink``: Boolean, True if the link (combination of uidb36 and - token) is valid or unused yet. - -.. function:: password_reset_complete(request[,template_name]) - - Presents a view which informs the user that the password has been - successfully changed. - - **URL name:** ``password_reset_complete`` - - **Optional arguments:** - - * ``template_name``: The full name of a template to display the view. - Defaults to :file:`registration/password_reset_complete.html`. - -Helper functions ----------------- - -.. currentmodule:: django.contrib.auth.views - -.. function:: redirect_to_login(next[, login_url, redirect_field_name]) - - Redirects to the login page, and then back to another URL after a - successful login. - - **Required arguments:** - - * ``next``: The URL to redirect to after a successful login. - - **Optional arguments:** - - * ``login_url``: The URL of the login page to redirect to. - Defaults to :setting:`settings.LOGIN_URL ` if not supplied. - - * ``redirect_field_name``: The name of a ``GET`` field containing the - URL to redirect to after log out. Overrides ``next`` if the given - ``GET`` parameter is passed. - - -.. _built-in-auth-forms: - -Built-in forms --------------- - -.. module:: django.contrib.auth.forms - -If you don't want to use the built-in views, but want the convenience of not -having to write forms for this functionality, the authentication system -provides several built-in forms located in :mod:`django.contrib.auth.forms`: - -.. class:: AdminPasswordChangeForm - - A form used in the admin interface to change a user's password. - -.. class:: AuthenticationForm - - A form for logging a user in. - -.. class:: PasswordChangeForm - - A form for allowing a user to change their password. - -.. class:: PasswordResetForm - - A form for generating and emailing a one-time use link to reset a - user's password. - -.. class:: SetPasswordForm - - A form that lets a user change his/her password without entering the old - password. - -.. class:: UserChangeForm - - A form used in the admin interface to change a user's information and - permissions. - -.. class:: UserCreationForm - - A form for creating a new user. - -Limiting access to logged-in users that pass a test ---------------------------------------------------- - -.. currentmodule:: django.contrib.auth.decorators - -To limit access based on certain permissions or some other test, you'd do -essentially the same thing as described in the previous section. - -The simple way is to run your test on :attr:`request.user -` in the view directly. For example, this view -checks to make sure the user is logged in and has the permission -``polls.can_vote``:: - - def my_view(request): - if not request.user.has_perm('polls.can_vote'): - return HttpResponse("You can't vote in this poll.") - # ... - -.. function:: user_passes_test(func, [login_url=None]) - - As a shortcut, you can use the convenient ``user_passes_test`` decorator:: - - from django.contrib.auth.decorators import user_passes_test - - @user_passes_test(lambda u: u.has_perm('polls.can_vote')) - def my_view(request): - ... - - We're using this particular test as a relatively simple example. However, - if you just want to test whether a permission is available to a user, you - can use the :func:`~django.contrib.auth.decorators.permission_required()` - decorator, described later in this document. - - :func:`~django.contrib.auth.decorators.user_passes_test` takes a required - argument: a callable that takes a - :class:`~django.contrib.auth.models.User` object and returns ``True`` if - the user is allowed to view the page. Note that - :func:`~django.contrib.auth.decorators.user_passes_test` does not - automatically check that the :class:`~django.contrib.auth.models.User` is - not anonymous. - - :func:`~django.contrib.auth.decorators.user_passes_test()` takes an - optional ``login_url`` argument, which lets you specify the URL for your - login page (:setting:`settings.LOGIN_URL ` by default). - - For example:: - - from django.contrib.auth.decorators import user_passes_test - - @user_passes_test(lambda u: u.has_perm('polls.can_vote'), login_url='/login/') - def my_view(request): - ... - -The permission_required decorator -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. function:: permission_required([login_url=None, raise_exception=False]) - - It's a relatively common task to check whether a user has a particular - permission. For that reason, Django provides a shortcut for that case: the - :func:`~django.contrib.auth.decorators.permission_required()` decorator. - Using this decorator, the earlier example can be written as:: - - from django.contrib.auth.decorators import permission_required - - @permission_required('polls.can_vote') - def my_view(request): - ... - - As for the :meth:`User.has_perm` method, permission names take the form - ``"."`` (i.e. ``polls.can_vote`` for a - permission on a model in the ``polls`` application). - - Note that :func:`~django.contrib.auth.decorators.permission_required()` - also takes an optional ``login_url`` parameter. Example:: - - from django.contrib.auth.decorators import permission_required - - @permission_required('polls.can_vote', login_url='/loginpage/') - def my_view(request): - ... - - As in the :func:`~decorators.login_required` decorator, ``login_url`` - defaults to :setting:`settings.LOGIN_URL `. - - .. versionchanged:: 1.4 - - Added ``raise_exception`` parameter. If given, the decorator will raise - :exc:`~django.core.exceptions.PermissionDenied`, prompting - :ref:`the 403 (HTTP Forbidden) view` instead of - redirecting to the login page. - -.. currentmodule:: django.contrib.auth - -Applying permissions to generic views -------------------------------------- - -To apply a permission to a :doc:`class-based generic view -`, decorate the :meth:`View.dispatch -` method on the class. See -:ref:`decorating-class-based-views` for details. - -.. _permissions: - -Permissions -=========== - -Django comes with a simple permissions system. It provides a way to assign -permissions to specific users and groups of users. - -It's used by the Django admin site, but you're welcome to use it in your own -code. - -The Django admin site uses permissions as follows: - -* Access to view the "add" form and add an object is limited to users with - the "add" permission for that type of object. -* Access to view the change list, view the "change" form and change an - object is limited to users with the "change" permission for that type of - object. -* Access to delete an object is limited to users with the "delete" - permission for that type of object. - -Permissions can be set not only per type of object, but also per specific -object instance. By using the -:meth:`~django.contrib.admin.ModelAdmin.has_add_permission`, -:meth:`~django.contrib.admin.ModelAdmin.has_change_permission` and -:meth:`~django.contrib.admin.ModelAdmin.has_delete_permission` methods provided -by the :class:`~django.contrib.admin.ModelAdmin` class, it is possible to -customize permissions for different object instances of the same type. - -Default permissions -------------------- - -When ``django.contrib.auth`` is listed in your :setting:`INSTALLED_APPS` -setting, it will ensure that three default permissions -- add, change and -delete -- are created for each Django model defined in one of your installed -applications. - -These permissions will be created when you run :djadmin:`manage.py syncdb -`; the first time you run ``syncdb`` after adding -``django.contrib.auth`` to :setting:`INSTALLED_APPS`, the default permissions -will be created for all previously-installed models, as well as for any new -models being installed at that time. Afterward, it will create default -permissions for new models each time you run :djadmin:`manage.py syncdb -`. - -Assuming you have an application with an -:attr:`~django.db.models.Options.app_label` ``foo`` and a model named ``Bar``, -to test for basic permissions you should use: - -* add: ``user.has_perm('foo.add_bar')`` -* change: ``user.has_perm('foo.change_bar')`` -* delete: ``user.has_perm('foo.delete_bar')`` - -.. _custom-permissions: - -Custom permissions ------------------- - -To create custom permissions for a given model object, use the ``permissions`` -:ref:`model Meta attribute `. - -This example Task model creates three custom permissions, i.e., actions users -can or cannot do with Task instances, specific to your application:: - - class Task(models.Model): - ... - class Meta: - permissions = ( - ("view_task", "Can see available tasks"), - ("change_task_status", "Can change the status of tasks"), - ("close_task", "Can remove a task by setting its status as closed"), - ) - -The only thing this does is create those extra permissions when you run -:djadmin:`manage.py syncdb `. Your code is in charge of checking the -value of these permissions when an user is trying to access the functionality -provided by the application (viewing tasks, changing the status of tasks, -closing tasks.) Continuing the above example, the following checks if a user may -view tasks:: - - user.has_perm('app.view_task') - -API reference -------------- - -.. currentmodule:: django.contrib.auth.models - -.. class:: models.Permission - -Fields -~~~~~~ - -:class:`~django.contrib.auth.models.Permission` objects have the following -fields: - -.. attribute:: Permission.name - - Required. 50 characters or fewer. Example: ``'Can vote'``. - -.. attribute:: Permission.content_type - - Required. A reference to the ``django_content_type`` database table, which - contains a record for each installed Django model. - -.. attribute:: Permission.codename - - Required. 100 characters or fewer. Example: ``'can_vote'``. - -Methods -~~~~~~~ - -:class:`~django.contrib.auth.models.Permission` objects have the standard -data-access methods like any other :doc:`Django model `. - -.. currentmodule:: django.contrib.auth - -Programmatically creating permissions -------------------------------------- - -While custom permissions can be defined within a model's ``Meta`` class, you -can also create permissions directly. For example, you can create the -``can_publish`` permission for a ``BlogPost`` model in ``myapp``:: - - from django.contrib.auth.models import Group, Permission - from django.contrib.contenttypes.models import ContentType - - content_type = ContentType.objects.get(app_label='myapp', model='BlogPost') - permission = Permission.objects.create(codename='can_publish', - name='Can Publish Posts', - content_type=content_type) - -The permission can then be assigned to a -:class:`~django.contrib.auth.models.User` via its ``user_permissions`` -attribute or to a :class:`~django.contrib.auth.models.Group` via its -``permissions`` attribute. - -Authentication data in templates -================================ - -The currently logged-in user and his/her permissions are made available in the -:doc:`template context ` when you use -:class:`~django.template.context.RequestContext`. - -.. admonition:: Technicality - - Technically, these variables are only made available in the template context - if you use :class:`~django.template.context.RequestContext` *and* your - :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting contains - ``"django.contrib.auth.context_processors.auth"``, which is default. For - more, see the :ref:`RequestContext docs `. - -Users ------ - -When rendering a template :class:`~django.template.context.RequestContext`, the -currently logged-in user, either a :class:`~django.contrib.auth.models.User` -instance or an :class:`~django.contrib.auth.models.AnonymousUser` instance, is -stored in the template variable ``{{ user }}``: - -.. code-block:: html+django - - {% if user.is_authenticated %} -

    Welcome, {{ user.username }}. Thanks for logging in.

    - {% else %} -

    Welcome, new user. Please log in.

    - {% endif %} - -This template context variable is not available if a ``RequestContext`` is not -being used. - -Permissions ------------ - -The currently logged-in user's permissions are stored in the template variable -``{{ perms }}``. This is an instance of -:class:`django.contrib.auth.context_processors.PermWrapper`, which is a -template-friendly proxy of permissions. - -In the ``{{ perms }}`` object, single-attribute lookup is a proxy to -:meth:`User.has_module_perms `. -This example would display ``True`` if the logged-in user had any permissions -in the ``foo`` app:: - - {{ perms.foo }} - -Two-level-attribute lookup is a proxy to -:meth:`User.has_perm `. This example -would display ``True`` if the logged-in user had the permission -``foo.can_vote``:: - - {{ perms.foo.can_vote }} - -Thus, you can check permissions in template ``{% if %}`` statements: - -.. code-block:: html+django - - {% if perms.foo %} -

    You have permission to do something in the foo app.

    - {% if perms.foo.can_vote %} -

    You can vote!

    - {% endif %} - {% if perms.foo.can_drive %} -

    You can drive!

    - {% endif %} - {% else %} -

    You don't have permission to do anything in the foo app.

    - {% endif %} - -.. versionadded:: 1.5 - Permission lookup by "if in". - -It is possible to also look permissions up by ``{% if in %}`` statements. -For example: - -.. code-block:: html+django - - {% if 'foo' in perms %} - {% if 'foo.can_vote' in perms %} -

    In lookup works, too.

    - {% endif %} - {% endif %} - -Groups -====== - -Groups are a generic way of categorizing users so you can apply permissions, or -some other label, to those users. A user can belong to any number of groups. - -A user in a group automatically has the permissions granted to that group. For -example, if the group ``Site editors`` has the permission -``can_edit_home_page``, any user in that group will have that permission. - -Beyond permissions, groups are a convenient way to categorize users to give -them some label, or extended functionality. For example, you could create a -group ``'Special users'``, and you could write code that could, say, give them -access to a members-only portion of your site, or send them members-only email -messages. - -API reference -------------- - -.. class:: models.Group - -Fields -~~~~~~ - -:class:`~django.contrib.auth.models.Group` objects have the following fields: - -.. attribute:: Group.name - - Required. 80 characters or fewer. Any characters are permitted. Example: - ``'Awesome Users'``. - -.. attribute:: Group.permissions - - Many-to-many field to :class:`~django.contrib.auth.models.Permissions`:: - - group.permissions = [permission_list] - group.permissions.add(permission, permission, ...) - group.permissions.remove(permission, permission, ...) - group.permissions.clear() - -.. _auth-custom-user: - -Customizing the User model -========================== - -.. versionadded:: 1.5 - -Some kinds of projects may have authentication requirements for which Django's -built-in :class:`~django.contrib.auth.models.User` model is not always -appropriate. For instance, on some sites it makes more sense to use an email -address as your identification token instead of a username. - -Django allows you to override the default User model by providing a value for -the :setting:`AUTH_USER_MODEL` setting that references a custom model:: - - AUTH_USER_MODEL = 'myapp.MyUser' - -This dotted pair describes the name of the Django app, and the name of the Django -model that you wish to use as your User model. - -.. admonition:: Warning - - Changing :setting:`AUTH_USER_MODEL` has a big effect on your database - structure. It changes the tables that are available, and it will affect the - construction of foreign keys and many-to-many relationships. If you intend - to set :setting:`AUTH_USER_MODEL`, you should set it before running - ``manage.py syncdb`` for the first time. - - If you have an existing project and you want to migrate to using a custom - User model, you may need to look into using a migration tool like South_ - to ease the transition. - -.. _South: http://south.aeracode.org - -Referencing the User model --------------------------- - -If you reference :class:`~django.contrib.auth.models.User` directly (for -example, by referring to it in a foreign key), your code will not work in -projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a -different User model. - -Instead of referring to :class:`~django.contrib.auth.models.User` directly, -you should reference the user model using -:func:`django.contrib.auth.get_user_model()`. This method will return the -currently active User model -- the custom User model if one is specified, or -:class:`~django.contrib.auth.User` otherwise. - -When you define a foreign key or many-to-many relations to the User model, -you should specify the custom model using the :setting:`AUTH_USER_MODEL` -setting. For example:: - - from django.conf import settings - from django.db import models - - class Article(models.Model) - author = models.ForeignKey(settings.AUTH_USER_MODEL) - -Specifying a custom User model ------------------------------- - -.. admonition:: Model design considerations - - Think carefully before handling information not directly related to - authentication in your custom User Model. - - It may be better to store app-specific user information in a model - that has a relation with the User model. That allows each app to specify - its own user data requirements without risking conflicts with other - apps. On the other hand, queries to retrieve this related information - will involve a database join, which may have an effect on performance. - -Django expects your custom User model to meet some minimum requirements. - -1. Your model must have a single unique field that can be used for - identification purposes. This can be a username, an email address, - or any other unique attribute. - -2. Your model must provide a way to address the user in a "short" and - "long" form. The most common interpretation of this would be to use - the user's given name as the "short" identifier, and the user's full - name as the "long" identifier. However, there are no constraints on - what these two methods return - if you want, they can return exactly - the same value. - -The easiest way to construct a compliant custom User model is to inherit from -:class:`~django.contrib.auth.models.AbstractBaseUser`. -:class:`~django.contrib.auth.models.AbstractBaseUser` provides the core -implementation of a `User` model, including hashed passwords and tokenized -password resets. You must then provide some key implementation details: - -.. class:: models.CustomUser - - .. attribute:: User.USERNAME_FIELD - - A string describing the name of the field on the User model that is - used as the unique identifier. This will usually be a username of - some kind, but it can also be an email address, or any other unique - identifier. In the following example, the field `identifier` is used - as the identifying field:: - - class MyUser(AbstractBaseUser): - identifier = models.CharField(max_length=40, unique=True, db_index=True) - ... - USERNAME_FIELD = 'identifier' - - .. attribute:: User.REQUIRED_FIELDS - - A list of the field names that *must* be provided when creating - a user. For example, here is the partial definition for a User model - that defines two required fields - a date of birth and height:: - - class MyUser(AbstractBaseUser): - ... - date_of_birth = models.DateField() - height = models.FloatField() - ... - REQUIRED_FIELDS = ['date_of_birth', 'height'] - - .. note:: - - ``REQUIRED_FIELDS`` must contain all required fields on your User - model, but should *not* contain the ``USERNAME_FIELD``. - - .. attribute:: User.is_active - - A boolean attribute that indicates whether the user is considered - "active". This attribute is provided as an attribute on - ``AbstractBaseUser`` defaulting to ``True``. How you choose to - implement it will depend on the details of your chosen auth backends. - See the documentation of the :attr:`attribute on the builtin user model - ` for details. - - .. method:: User.get_full_name(): - - A longer formal identifier for the user. A common interpretation - would be the full name name of the user, but it can be any string that - identifies the user. - - .. method:: User.get_short_name(): - - A short, informal identifier for the user. A common interpretation - would be the first name of the user, but it can be any string that - identifies the user in an informal way. It may also return the same - value as :meth:`django.contrib.auth.User.get_full_name()`. - -The following methods are available on any subclass of -:class:`~django.contrib.auth.models.AbstractBaseUser`: - -.. class:: models.AbstractBaseUser - - .. method:: models.AbstractBaseUser.get_username() - - Returns the value of the field nominated by ``USERNAME_FIELD``. - - .. method:: models.AbstractBaseUser.is_anonymous() - - Always returns ``False``. This is a way of differentiating - from :class:`~django.contrib.auth.models.AnonymousUser` objects. - Generally, you should prefer using - :meth:`~django.contrib.auth.models.AbstractBaseUser.is_authenticated()` to this - method. - - .. method:: models.AbstractBaseUser.is_authenticated() - - Always returns ``True``. This is a way to tell if the user has been - authenticated. This does not imply any permissions, and doesn't check - if the user is active - it only indicates that the user has provided a - valid username and password. - - .. method:: models.AbstractBaseUser.set_password(raw_password) - - Sets the user's password to the given raw string, taking care of the - password hashing. Doesn't save the - :class:`~django.contrib.auth.models.AbstractBaseUser` object. - - .. method:: models.AbstractBaseUser.check_password(raw_password) - - Returns ``True`` if the given raw string is the correct password for - the user. (This takes care of the password hashing in making the - comparison.) - - .. method:: models.AbstractBaseUser.set_unusable_password() - - Marks the user as having no password set. This isn't the same as - having a blank string for a password. - :meth:`~django.contrib.auth.models.AbstractBaseUser.check_password()` for this user - will never return ``True``. Doesn't save the - :class:`~django.contrib.auth.models.AbstractBaseUser` object. - - You may need this if authentication for your application takes place - against an existing external source such as an LDAP directory. - - .. method:: models.AbstractBaseUser.has_usable_password() - - Returns ``False`` if - :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()` has - been called for this user. - - -You should also define a custom manager for your User model. If your User -model defines `username` and `email` fields the same as Django's default User, -you can just install Django's -:class:`~django.contrib.auth.models.UserManager`; however, if your User model -defines different fields, you will need to define a custom manager that -extends :class:`~django.contrib.auth.models.BaseUserManager` providing two -additional methods: - -.. class:: models.CustomUserManager - - .. method:: models.CustomUserManager.create_user(*username_field*, password=None, **other_fields) - - The prototype of `create_user()` should accept the username field, - plus all required fields as arguments. For example, if your user model - uses `email` as the username field, and has `date_of_birth` as a required - fields, then create_user should be defined as:: - - def create_user(self, email, date_of_birth, password=None): - # create user here - - .. method:: models.CustomUserManager.create_superuser(*username_field*, password, **other_fields) - - The prototype of `create_superuser()` should accept the username field, - plus all required fields as arguments. For example, if your user model - uses `email` as the username field, and has `date_of_birth` as a required - fields, then create_superuser should be defined as:: - - def create_superuser(self, email, date_of_birth, password): - # create superuser here - - Unlike `create_user()`, `create_superuser()` *must* require the caller - to provider a password. - -:class:`~django.contrib.auth.models.BaseUserManager` provides the following -utility methods: - -.. class:: models.BaseUserManager - - .. method:: models.BaseUserManager.normalize_email(email) - - A classmethod that normalizes email addresses by lowercasing - the domain portion of the email address. - - .. method:: models.BaseUserManager.get_by_natural_key(username) - - Retrieves a user instance using the contents of the field - nominated by ``USERNAME_FIELD``. - - .. method:: models.BaseUserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') - - Returns a random password with the given length and given string of - allowed characters. (Note that the default value of ``allowed_chars`` - doesn't contain letters that can cause user confusion, including: - - * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase - letter L, uppercase letter i, and the number one) - * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o, - and zero) - -Extending Django's default User -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you're entirely happy with Django's :class:`~django.contrib.auth.models.User` -model and you just want to add some additional profile information, you can -simply subclass :class:`~django.contrib.auth.models.AbstractUser` and add your -custom profile fields. - -Custom users and the built-in auth forms -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -As you may expect, built-in Django's :ref:`forms ` -and :ref:`views ` make certain assumptions about -the user model that they are working with. - -If your user model doesn't follow the same assumptions, it may be necessary to define -a replacement form, and pass that form in as part of the configuration of the -auth views. - -* :class:`~django.contrib.auth.forms.UserCreationForm` - - Depends on the :class:`~django.contrib.auth.models.User` model. - Must be re-written for any custom user model. - -* :class:`~django.contrib.auth.forms.UserChangeForm` - - Depends on the :class:`~django.contrib.auth.models.User` model. - Must be re-written for any custom user model. - -* :class:`~django.contrib.auth.forms.AuthenticationForm` - - Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`, - and will adapt to use the field defined in `USERNAME_FIELD`. - -* :class:`~django.contrib.auth.forms.PasswordResetForm` - - Assumes that the user model has an integer primary key, has a field named - `email` that can be used to identify the user, and a boolean field - named `is_active` to prevent password resets for inactive users. - -* :class:`~django.contrib.auth.forms.SetPasswordForm` - - Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` - -* :class:`~django.contrib.auth.forms.PasswordChangeForm` - - Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` - -* :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` - - Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` - - -Custom users and django.contrib.admin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you want your custom User model to also work with Admin, your User model must -define some additional attributes and methods. These methods allow the admin to -control access of the User to admin content: - -.. attribute:: User.is_staff - - Returns True if the user is allowed to have access to the admin site. - -.. attribute:: User.is_active - - Returns True if the user account is currently active. - -.. method:: User.has_perm(perm, obj=None): - - Returns True if the user has the named permission. If `obj` is - provided, the permission needs to be checked against a specific object - instance. - -.. method:: User.has_module_perms(app_label): - - Returns True if the user has permission to access models in - the given app. - -You will also need to register your custom User model with the admin. If -your custom User model extends :class:`~django.contrib.auth.models.AbstractUser`, -you can use Django's existing :class:`~django.contrib.auth.admin.UserAdmin` -class. However, if your User model extends -:class:`~django.contrib.auth.models.AbstractBaseUser`, you'll need to define -a custom ModelAdmin class. It may be possible to subclass the default -:class:`~django.contrib.auth.admin.UserAdmin`; however, you'll need to -override any of the definitions that refer to fields on -:class:`~django.contrib.auth.models.AbstractUser` that aren't on your -custom User class. - -Custom users and permissions -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To make it easy to include Django's permission framework into your own User -class, Django provides :class:`~django.contrib.auth.model.PermissionsMixin`. -This is an abstract model you can include in the class heirarchy for your User -model, giving you all the methods and database fields necessary to support -Django's permission model. - -:class:`~django.contrib.auth.model.PermissionsMixin` provides the following -methods and attributes: - -.. class:: models.PermissionsMixin - - .. attribute:: models.PermissionsMixin.is_superuser - - Boolean. Designates that this user has all permissions without - explicitly assigning them. - - .. method:: models.PermissionsMixin.get_group_permissions(obj=None) - - Returns a set of permission strings that the user has, through his/her - groups. - - If ``obj`` is passed in, only returns the group permissions for - this specific object. - - .. method:: models.PermissionsMixin.get_all_permissions(obj=None) - - Returns a set of permission strings that the user has, both through - group and user permissions. - - If ``obj`` is passed in, only returns the permissions for this - specific object. - - .. method:: models.PermissionsMixin.has_perm(perm, obj=None) - - Returns ``True`` if the user has the specified permission, where perm is - in the format ``"."`` (see - `permissions`_). If the user is inactive, this method will - always return ``False``. - - If ``obj`` is passed in, this method won't check for a permission for - the model, but for this specific object. - - .. method:: models.PermissionsMixin.has_perms(perm_list, obj=None) - - Returns ``True`` if the user has each of the specified permissions, - where each perm is in the format - ``"."``. If the user is inactive, - this method will always return ``False``. - - If ``obj`` is passed in, this method won't check for permissions for - the model, but for the specific object. - - .. method:: models.PermissionsMixin.has_module_perms(package_name) - - Returns ``True`` if the user has any permissions in the given package - (the Django app label). If the user is inactive, this method will - always return ``False``. - -.. admonition:: ModelBackend - - If you don't include the - :class:`~django.contrib.auth.model.PermissionsMixin`, you must ensure you - don't invoke the permissions methods on ``ModelBackend``. ``ModelBackend`` - assumes that certain fields are available on your user model. If your User - model doesn't provide those fields, you will receive database errors when - you check permissions. - -Custom users and Proxy models -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -One limitation of custom User models is that installing a custom User model -will break any proxy model extending :class:`~django.contrib.auth.models.User`. -Proxy models must be based on a concrete base class; by defining a custom User -model, you remove the ability of Django to reliably identify the base class. - -If your project uses proxy models, you must either modify the proxy to extend -the User model that is currently in use in your project, or merge your proxy's -behavior into your User subclass. - -Custom users and signals -~~~~~~~~~~~~~~~~~~~~~~~~ - -Another limitation of custom User models is that you can't use -:func:`django.contrib.auth.get_user_model()` as the sender or target of a signal -handler. Instead, you must register the handler with the actual User model. - -Custom users and testing/fixtures -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you are writing an application that interacts with the User model, you must -take some precautions to ensure that your test suite will run regardless of -the User model that is being used by a project. Any test that instantiates an -instance of User will fail if the User model has been swapped out. This -includes any attempt to create an instance of User with a fixture. - -To ensure that your test suite will pass in any project configuration, -``django.contrib.auth.tests.utils`` defines a ``@skipIfCustomUser`` decorator. -This decorator will cause a test case to be skipped if any User model other -than the default Django user is in use. This decorator can be applied to a -single test, or to an entire test class. - -Depending on your application, tests may also be needed to be added to ensure -that the application works with *any* user model, not just the default User -model. To assist with this, Django provides two substitute user models that -can be used in test suites: - -* :class:`django.contrib.auth.tests.custom_user.CustomUser`, a custom user - model that uses an ``email`` field as the username, and has a basic - admin-compliant permissions setup - -* :class:`django.contrib.auth.tests.custom_user.ExtensionUser`, a custom - user model that extends :class:`~django.contrib.auth.models.AbstractUser`, - adding a ``date_of_birth`` field. - -You can then use the ``@override_settings`` decorator to make that test run -with the custom User model. For example, here is a skeleton for a test that -would test three possible User models -- the default, plus the two User -models provided by ``auth`` app:: - - from django.contrib.auth.tests.utils import skipIfCustomUser - from django.test import TestCase - from django.test.utils import override_settings - - - class ApplicationTestCase(TestCase): - @skipIfCustomUser - def test_normal_user(self): - "Run tests for the normal user model" - self.assertSomething() - - @override_settings(AUTH_USER_MODEL='auth.CustomUser') - def test_custom_user(self): - "Run tests for a custom user model with email-based authentication" - self.assertSomething() - - @override_settings(AUTH_USER_MODEL='auth.ExtensionUser') - def test_extension_user(self): - "Run tests for a simple extension of the built-in User." - self.assertSomething() - - -A full example --------------- - -Here is an example of an admin-compliant custom user app. This user model uses -an email address as the username, and has a required date of birth; it -provides no permission checking, beyond a simple `admin` flag on the user -account. This model would be compatible with all the built-in auth forms and -views, except for the User creation forms. - -This code would all live in a ``models.py`` file for a custom -authentication app:: - - from django.db import models - from django.contrib.auth.models import ( - BaseUserManager, AbstractBaseUser - ) - - - class MyUserManager(BaseUserManager): - def create_user(self, email, date_of_birth, password=None): - """ - Creates and saves a User with the given email, date of - birth and password. - """ - if not email: - raise ValueError('Users must have an email address') - - user = self.model( - email=MyUserManager.normalize_email(email), - date_of_birth=date_of_birth, - ) - - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, email, date_of_birth, password): - """ - Creates and saves a superuser with the given email, date of - birth and password. - """ - user = self.create_user(email, - password=password, - date_of_birth=date_of_birth - ) - user.is_admin = True - user.save(using=self._db) - return user - - - class MyUser(AbstractBaseUser): - email = models.EmailField( - verbose_name='email address', - max_length=255, - unique=True, - db_index=True, - ) - date_of_birth = models.DateField() - is_active = models.BooleanField(default=True) - is_admin = models.BooleanField(default=False) - - objects = MyUserManager() - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['date_of_birth'] - - def get_full_name(self): - # The user is identified by their email address - return self.email - - def get_short_name(self): - # The user is identified by their email address - return self.email - - def __unicode__(self): - return self.email - - def has_perm(self, perm, obj=None): - "Does the user have a specific permission?" - # Simplest possible answer: Yes, always - return True - - def has_module_perms(self, app_label): - "Does the user have permissions to view the app `app_label`?" - # Simplest possible answer: Yes, always - return True - - @property - def is_staff(self): - "Is the user a member of staff?" - # Simplest possible answer: All admins are staff - return self.is_admin - -Then, to register this custom User model with Django's admin, the following -code would be required in the app's ``admin.py`` file:: - - from django import forms - from django.contrib import admin - from django.contrib.auth.models import Group - from django.contrib.auth.admin import UserAdmin - from django.contrib.auth.forms import ReadOnlyPasswordHashField - - from customauth.models import MyUser - - - class UserCreationForm(forms.ModelForm): - """A form for creating new users. Includes all the required - fields, plus a repeated password.""" - password1 = forms.CharField(label='Password', widget=forms.PasswordInput) - password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) - - class Meta: - model = MyUser - fields = ('email', 'date_of_birth') - - def clean_password2(self): - # Check that the two password entries match - password1 = self.cleaned_data.get("password1") - password2 = self.cleaned_data.get("password2") - if password1 and password2 and password1 != password2: - raise forms.ValidationError("Passwords don't match") - return password2 - - def save(self, commit=True): - # Save the provided password in hashed format - user = super(UserCreationForm, self).save(commit=False) - user.set_password(self.cleaned_data["password1"]) - if commit: - user.save() - return user - - - class UserChangeForm(forms.ModelForm): - """A form for updating users. Includes all the fields on - the user, but replaces the password field with admin's - password hash display field. - """ - password = ReadOnlyPasswordHashField() - - class Meta: - model = MyUser - - def clean_password(self): - # Regardless of what the user provides, return the initial value. - # This is done here, rather than on the field, because the - # field does not have access to the initial value - return self.initial["password"] - - - class MyUserAdmin(UserAdmin): - # The forms to add and change user instances - form = UserChangeForm - add_form = UserCreationForm - - # The fields to be used in displaying the User model. - # These override the definitions on the base UserAdmin - # that reference specific fields on auth.User. - list_display = ('email', 'date_of_birth', 'is_admin') - list_filter = ('is_admin',) - fieldsets = ( - (None, {'fields': ('email', 'password')}), - ('Personal info', {'fields': ('date_of_birth',)}), - ('Permissions', {'fields': ('is_admin',)}), - ('Important dates', {'fields': ('last_login',)}), - ) - add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ('email', 'date_of_birth', 'password1', 'password2')} - ), - ) - search_fields = ('email',) - ordering = ('email',) - filter_horizontal = () - - # Now register the new UserAdmin... - admin.site.register(MyUser, MyUserAdmin) - # ... and, since we're not using Django's builtin permissions, - # unregister the Group model from admin. - admin.site.unregister(Group) - -.. _authentication-backends: - -Other authentication sources -============================ - -The authentication that comes with Django is good enough for most common cases, -but you may have the need to hook into another authentication source -- that -is, another source of usernames and passwords or authentication methods. - -For example, your company may already have an LDAP setup that stores a username -and password for every employee. It'd be a hassle for both the network -administrator and the users themselves if users had separate accounts in LDAP -and the Django-based applications. - -So, to handle situations like this, the Django authentication system lets you -plug in other authentication sources. You can override Django's default -database-based scheme, or you can use the default system in tandem with other -systems. - -See the :doc:`authentication backend reference ` -for information on the authentication backends included with Django. - -Specifying authentication backends ----------------------------------- - -Behind the scenes, Django maintains a list of "authentication backends" that it -checks for authentication. When somebody calls -:func:`django.contrib.auth.authenticate()` -- as described in :ref:`How to log -a user in ` above -- Django tries authenticating across -all of its authentication backends. If the first authentication method fails, -Django tries the second one, and so on, until all backends have been attempted. - -The list of authentication backends to use is specified in the -:setting:`AUTHENTICATION_BACKENDS` setting. This should be a tuple of Python -path names that point to Python classes that know how to authenticate. These -classes can be anywhere on your Python path. - -By default, :setting:`AUTHENTICATION_BACKENDS` is set to:: - - ('django.contrib.auth.backends.ModelBackend',) - -That's the basic authentication backend that checks the Django users database -and queries the builtin permissions. It does not provide protection against -brute force attacks via any rate limiting mechanism. You may either implement -your own rate limiting mechanism in a custom auth backend, or use the -mechanisms provided by most Web servers. - -The order of :setting:`AUTHENTICATION_BACKENDS` matters, so if the same -username and password is valid in multiple backends, Django will stop -processing at the first positive match. - -.. note:: - - Once a user has authenticated, Django stores which backend was used to - authenticate the user in the user's session, and re-uses the same backend - for the duration of that session whenever access to the currently - authenticated user is needed. This effectively means that authentication - sources are cached on a per-session basis, so if you change - :setting:`AUTHENTICATION_BACKENDS`, you'll need to clear out session data if - you need to force users to re-authenticate using different methods. A simple - way to do that is simply to execute ``Session.objects.all().delete()``. - -.. versionadded:: 1.6 - -If a backend raises a :class:`~django.core.exceptions.PermissionDenied` -exception, authentication will immediately fail. Django won't check the -backends that follow. - -Writing an authentication backend ---------------------------------- - -An authentication backend is a class that implements two required methods: -``get_user(user_id)`` and ``authenticate(**credentials)``, as well as a set of -optional permission related :ref:`authorization methods `. - -The ``get_user`` method takes a ``user_id`` -- which could be a username, -database ID or whatever -- and returns a ``User`` object. - -The ``authenticate`` method takes credentials as keyword arguments. Most of -the time, it'll just look like this:: - - class MyBackend(object): - def authenticate(self, username=None, password=None): - # Check the username/password and return a User. - -But it could also authenticate a token, like so:: - - class MyBackend(object): - def authenticate(self, token=None): - # Check the token and return a User. - -Either way, ``authenticate`` should check the credentials it gets, and it -should return a ``User`` object that matches those credentials, if the -credentials are valid. If they're not valid, it should return ``None``. - -The Django admin system is tightly coupled to the Django ``User`` object -described at the beginning of this document. For now, the best way to deal with -this is to create a Django ``User`` object for each user that exists for your -backend (e.g., in your LDAP directory, your external SQL database, etc.) You -can either write a script to do this in advance, or your ``authenticate`` -method can do it the first time a user logs in. - -Here's an example backend that authenticates against a username and password -variable defined in your ``settings.py`` file and creates a Django ``User`` -object the first time a user authenticates:: - - from django.conf import settings - from django.contrib.auth.models import User, check_password - - class SettingsBackend(object): - """ - Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD. - - Use the login name, and a hash of the password. For example: - - ADMIN_LOGIN = 'admin' - ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de' - """ - - def authenticate(self, username=None, password=None): - login_valid = (settings.ADMIN_LOGIN == username) - pwd_valid = check_password(password, settings.ADMIN_PASSWORD) - if login_valid and pwd_valid: - try: - user = User.objects.get(username=username) - except User.DoesNotExist: - # Create a new user. Note that we can set password - # to anything, because it won't be checked; the password - # from settings.py will. - user = User(username=username, password='get from settings.py') - user.is_staff = True - user.is_superuser = True - user.save() - return user - return None - - def get_user(self, user_id): - try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: - return None - -.. _authorization_methods: - -Handling authorization in custom backends ------------------------------------------ - -Custom auth backends can provide their own permissions. - -The user model will delegate permission lookup functions -(:meth:`~django.contrib.auth.models.User.get_group_permissions()`, -:meth:`~django.contrib.auth.models.User.get_all_permissions()`, -:meth:`~django.contrib.auth.models.User.has_perm()`, and -:meth:`~django.contrib.auth.models.User.has_module_perms()`) to any -authentication backend that implements these functions. - -The permissions given to the user will be the superset of all permissions -returned by all backends. That is, Django grants a permission to a user that -any one backend grants. - -The simple backend above could implement permissions for the magic admin -fairly simply:: - - class SettingsBackend(object): - - # ... - - def has_perm(self, user_obj, perm, obj=None): - if user_obj.username == settings.ADMIN_LOGIN: - return True - else: - return False - -This gives full permissions to the user granted access in the above example. -Notice that in addition to the same arguments given to the associated -:class:`django.contrib.auth.models.User` functions, the backend auth functions -all take the user object, which may be an anonymous user, as an argument. - -A full authorization implementation can be found in the ``ModelBackend`` class -in `django/contrib/auth/backends.py`_, which is the default backend and queries -the ``auth_permission`` table most of the time. If you wish to provide -custom behavior for only part of the backend API, you can take advantage of -Python inheritence and subclass ``ModelBackend`` instead of implementing the -complete API in a custom backend. - -.. _django/contrib/auth/backends.py: https://github.com/django/django/blob/master/django/contrib/auth/backends.py - -.. _anonymous_auth: - -Authorization for anonymous users -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An anonymous user is one that is not authenticated i.e. they have provided no -valid authentication details. However, that does not necessarily mean they are -not authorized to do anything. At the most basic level, most Web sites -authorize anonymous users to browse most of the site, and many allow anonymous -posting of comments etc. - -Django's permission framework does not have a place to store permissions for -anonymous users. However, the user object passed to an authentication backend -may be an :class:`django.contrib.auth.models.AnonymousUser` object, allowing -the backend to specify custom authorization behavior for anonymous users. This -is especially useful for the authors of re-usable apps, who can delegate all -questions of authorization to the auth backend, rather than needing settings, -for example, to control anonymous access. - -.. _inactive_auth: - -Authorization for inactive users -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An inactive user is a one that is authenticated but has its attribute -``is_active`` set to ``False``. However this does not mean they are not -authorized to do anything. For example they are allowed to activate their -account. - -The support for anonymous users in the permission system allows for a scenario -where anonymous users have permissions to do something while inactive -authenticated users do not. - -Do not forget to test for the ``is_active`` attribute of the user in your own -backend permission methods. - - -Handling object permissions -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Django's permission framework has a foundation for object permissions, though -there is no implementation for it in the core. That means that checking for -object permissions will always return ``False`` or an empty list (depending on -the check performed). An authentication backend will receive the keyword -parameters ``obj`` and ``user_obj`` for each object related authorization -method and can return the object level permission as appropriate. diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt new file mode 100644 index 0000000000..5f48e82e2b --- /dev/null +++ b/docs/topics/auth/customizing.txt @@ -0,0 +1,1074 @@ +==================================== +Customizing authentication in Django +==================================== + +The authentication that comes with Django is good enough for most common cases, +but you may have needs not met by the out-of-the-box defaults. To customize +authentication to your projects needs involves understanding what points of the +provided system are extendible or replaceable. This document provides details +about how the auth system can be customized. + +:ref:`Authentication backends ` provide an extensible +system for when a username and password stored with the User model need +to be authenticated against a different service than Django's default. + +You can give your models :ref:`custom permissions ` that can be +checked through Django's authorization system. + +You can :ref:`extend ` the default User model, or :ref:`substitute +` a completely customized model. + +.. _authentication-backends: + +Other authentication sources +============================ + +There may be times you have the need to hook into another authentication source +-- that is, another source of usernames and passwords or authentication +methods. + +For example, your company may already have an LDAP setup that stores a username +and password for every employee. It'd be a hassle for both the network +administrator and the users themselves if users had separate accounts in LDAP +and the Django-based applications. + +So, to handle situations like this, the Django authentication system lets you +plug in other authentication sources. You can override Django's default +database-based scheme, or you can use the default system in tandem with other +systems. + +See the `authentication backend reference +` for information on the authentication +backends included with Django. + +Specifying authentication backends +---------------------------------- + +Behind the scenes, Django maintains a list of "authentication backends" that it +checks for authentication. When somebody calls +:func:`django.contrib.auth.authenticate()` -- as described in :ref:`How to log +a user in ` above -- Django tries authenticating across +all of its authentication backends. If the first authentication method fails, +Django tries the second one, and so on, until all backends have been attempted. + +The list of authentication backends to use is specified in the +:setting:`AUTHENTICATION_BACKENDS` setting. This should be a tuple of Python +path names that point to Python classes that know how to authenticate. These +classes can be anywhere on your Python path. + +By default, :setting:`AUTHENTICATION_BACKENDS` is set to:: + + ('django.contrib.auth.backends.ModelBackend',) + +That's the basic authentication backend that checks the Django users database +and queries the built-in permissions. It does not provide protection against +brute force attacks via any rate limiting mechanism. You may either implement +your own rate limiting mechanism in a custom auth backend, or use the +mechanisms provided by most Web servers. + +The order of :setting:`AUTHENTICATION_BACKENDS` matters, so if the same +username and password is valid in multiple backends, Django will stop +processing at the first positive match. + +.. note:: + + Once a user has authenticated, Django stores which backend was used to + authenticate the user in the user's session, and re-uses the same backend + for the duration of that session whenever access to the currently + authenticated user is needed. This effectively means that authentication + sources are cached on a per-session basis, so if you change + :setting:`AUTHENTICATION_BACKENDS`, you'll need to clear out session data if + you need to force users to re-authenticate using different methods. A simple + way to do that is simply to execute ``Session.objects.all().delete()``. + +.. versionadded:: 1.6 + +If a backend raises a :class:`~django.core.exceptions.PermissionDenied` +exception, authentication will immediately fail. Django won't check the +backends that follow. + +Writing an authentication backend +--------------------------------- + +An authentication backend is a class that implements two required methods: +``get_user(user_id)`` and ``authenticate(**credentials)``, as well as a set of +optional permission related :ref:`authorization methods `. + +The ``get_user`` method takes a ``user_id`` -- which could be a username, +database ID or whatever -- and returns a ``User`` object. + +The ``authenticate`` method takes credentials as keyword arguments. Most of +the time, it'll just look like this:: + + class MyBackend(object): + def authenticate(self, username=None, password=None): + # Check the username/password and return a User. + +But it could also authenticate a token, like so:: + + class MyBackend(object): + def authenticate(self, token=None): + # Check the token and return a User. + +Either way, ``authenticate`` should check the credentials it gets, and it +should return a ``User`` object that matches those credentials, if the +credentials are valid. If they're not valid, it should return ``None``. + +The Django admin system is tightly coupled to the Django ``User`` object +described at the beginning of this document. For now, the best way to deal with +this is to create a Django ``User`` object for each user that exists for your +backend (e.g., in your LDAP directory, your external SQL database, etc.) You +can either write a script to do this in advance, or your ``authenticate`` +method can do it the first time a user logs in. + +Here's an example backend that authenticates against a username and password +variable defined in your ``settings.py`` file and creates a Django ``User`` +object the first time a user authenticates:: + + from django.conf import settings + from django.contrib.auth.models import User, check_password + + class SettingsBackend(object): + """ + Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD. + + Use the login name, and a hash of the password. For example: + + ADMIN_LOGIN = 'admin' + ADMIN_PASSWORD = 'sha1$4e987$afbcf42e21bd417fb71db8c66b321e9fc33051de' + """ + + def authenticate(self, username=None, password=None): + login_valid = (settings.ADMIN_LOGIN == username) + pwd_valid = check_password(password, settings.ADMIN_PASSWORD) + if login_valid and pwd_valid: + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + # Create a new user. Note that we can set password + # to anything, because it won't be checked; the password + # from settings.py will. + user = User(username=username, password='get from settings.py') + user.is_staff = True + user.is_superuser = True + user.save() + return user + return None + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None + +.. _authorization_methods: + +Handling authorization in custom backends +----------------------------------------- + +Custom auth backends can provide their own permissions. + +The user model will delegate permission lookup functions +(:meth:`~django.contrib.auth.models.User.get_group_permissions()`, +:meth:`~django.contrib.auth.models.User.get_all_permissions()`, +:meth:`~django.contrib.auth.models.User.has_perm()`, and +:meth:`~django.contrib.auth.models.User.has_module_perms()`) to any +authentication backend that implements these functions. + +The permissions given to the user will be the superset of all permissions +returned by all backends. That is, Django grants a permission to a user that +any one backend grants. + +The simple backend above could implement permissions for the magic admin +fairly simply:: + + class SettingsBackend(object): + + # ... + + def has_perm(self, user_obj, perm, obj=None): + if user_obj.username == settings.ADMIN_LOGIN: + return True + else: + return False + +This gives full permissions to the user granted access in the above example. +Notice that in addition to the same arguments given to the associated +:class:`django.contrib.auth.models.User` functions, the backend auth functions +all take the user object, which may be an anonymous user, as an argument. + +A full authorization implementation can be found in the ``ModelBackend`` class +in `django/contrib/auth/backends.py`_, which is the default backend and queries +the ``auth_permission`` table most of the time. If you wish to provide +custom behavior for only part of the backend API, you can take advantage of +Python inheritance and subclass ``ModelBackend`` instead of implementing the +complete API in a custom backend. + +.. _django/contrib/auth/backends.py: https://github.com/django/django/blob/master/django/contrib/auth/backends.py + +.. _anonymous_auth: + +Authorization for anonymous users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An anonymous user is one that is not authenticated i.e. they have provided no +valid authentication details. However, that does not necessarily mean they are +not authorized to do anything. At the most basic level, most Web sites +authorize anonymous users to browse most of the site, and many allow anonymous +posting of comments etc. + +Django's permission framework does not have a place to store permissions for +anonymous users. However, the user object passed to an authentication backend +may be an :class:`django.contrib.auth.models.AnonymousUser` object, allowing +the backend to specify custom authorization behavior for anonymous users. This +is especially useful for the authors of re-usable apps, who can delegate all +questions of authorization to the auth backend, rather than needing settings, +for example, to control anonymous access. + +.. _inactive_auth: + +Authorization for inactive users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +An inactive user is a one that is authenticated but has its attribute +``is_active`` set to ``False``. However this does not mean they are not +authorized to do anything. For example they are allowed to activate their +account. + +The support for anonymous users in the permission system allows for a scenario +where anonymous users have permissions to do something while inactive +authenticated users do not. + +Do not forget to test for the ``is_active`` attribute of the user in your own +backend permission methods. + + +Handling object permissions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django's permission framework has a foundation for object permissions, though +there is no implementation for it in the core. That means that checking for +object permissions will always return ``False`` or an empty list (depending on +the check performed). An authentication backend will receive the keyword +parameters ``obj`` and ``user_obj`` for each object related authorization +method and can return the object level permission as appropriate. + +.. _custom-permissions: + +Custom permissions +================== + +To create custom permissions for a given model object, use the ``permissions`` +:ref:`model Meta attribute `. + +This example Task model creates three custom permissions, i.e., actions users +can or cannot do with Task instances, specific to your application:: + + class Task(models.Model): + ... + class Meta: + permissions = ( + ("view_task", "Can see available tasks"), + ("change_task_status", "Can change the status of tasks"), + ("close_task", "Can remove a task by setting its status as closed"), + ) + +The only thing this does is create those extra permissions when you run +:djadmin:`manage.py syncdb `. Your code is in charge of checking the +value of these permissions when an user is trying to access the functionality +provided by the application (viewing tasks, changing the status of tasks, +closing tasks.) Continuing the above example, the following checks if a user may +view tasks:: + + user.has_perm('app.view_task') + +.. _extending-user: + +Extending the existing User model +================================= + +There are two ways to extend the default +:class:`~django.contrib.auth.models.User` model without substituting your own +model. If the changes you need are purely behavioral, and don't require any +change to what is stored in the database, you can create a :ref:`proxy model +` based on :class:`~django.contrib.auth.models.User`. This +allows for any of the features offered by proxy models including default +ordering, custom managers, or custom model methods. + +If you wish to store information related to ``User``, you can use a :ref:`one-to-one +relationship ` to a model containing the fields for +additional information. This one-to-one model is often called a profile model, +as it might store non-auth related information about a site user. For example +you might create an Employee model:: + + from django.contrib.auth.models import User + + class Employee(models.Model): + user = models.OneToOneField(User) + department = models.CharField(max_length=100) + +Assuming an existing Employee Fred Smith who has both a User and Employee +model, you can access the related information using Django's standard related +model conventions:: + + >>> u = User.objects.get(username='fsmith') + >>> freds_department = u.employee.department + +To add a profile model's fields to the user page in the admin, define an +:class:`~django.contrib.admin.InlineModelAdmin` (for this example, we'll use a +:class:`~django.contrib.admin.StackedInline`) in your app's ``admin.py`` and +add it to a ``UserAdmin`` class which is registered with the +:class:`~django.contrib.auth.models.User` class:: + + from django.contrib import admin + from django.contrib.auth.admin import UserAdmin + from django.contrib.auth.models import User + + from my_user_profile_app.models import Employee + + # Define an inline admin descriptor for Employee model + # which acts a bit like a singleton + class EmployeeInline(admin.StackedInline): + model = Employee + can_delete = False + verbose_name_plural = 'employee' + + # Define a new User admin + class UserAdmin(UserAdmin): + inlines = (EmployeeInline, ) + + # Re-register UserAdmin + admin.site.unregister(User) + admin.site.register(User, UserAdmin) + +These profile models are not special in any way - they are just Django models that +happen to have a one-to-one link with a User model. As such, they do not get +auto created when a user is created, but +a :attr:`django.db.models.signals.post_save` could be used to create or update +related models as appropriate. + +Note that using related models results in additional queries or joins to +retrieve the related data, and depending on your needs substituting the User +model and adding the related fields may be your better option. However +existing links to the default User model within your project's apps may justify +the extra database load. + +.. _auth-profiles: + +.. deprecated:: 1.5 + With the introduction of :ref:`custom User models `, + the use of :setting:`AUTH_PROFILE_MODULE` to define a single profile + model is no longer supported. See the + :doc:`Django 1.5 release notes` for more information. + +Prior to 1.5, a single profile model could be specified site-wide with the +setting :setting:`AUTH_PROFILE_MODULE` with a string consisting of the +following items, separated by a dot: + +1. The name of the application (case sensitive) in which the user + profile model is defined (in other words, the + name which was passed to :djadmin:`manage.py startapp ` to create + the application). + +2. The name of the model (not case sensitive) class. + +For example, if the profile model was a class named ``UserProfile`` and was +defined inside an application named ``accounts``, the appropriate setting would +be:: + + AUTH_PROFILE_MODULE = 'accounts.UserProfile' + +When a user profile model has been defined and specified in this manner, each +:class:`~django.contrib.auth.models.User` object will have a method -- +:class:`~django.contrib.auth.models.User.get_profile()` -- which returns the +instance of the user profile model associated with that +:class:`~django.contrib.auth.models.User`. + +The method :class:`~django.contrib.auth.models.User.get_profile()` +does not create a profile if one does not exist. + +.. _auth-custom-user: + +Substituting a custom User model +================================ + +.. versionadded:: 1.5 + +Some kinds of projects may have authentication requirements for which Django's +built-in :class:`~django.contrib.auth.models.User` model is not always +appropriate. For instance, on some sites it makes more sense to use an email +address as your identification token instead of a username. + +Django allows you to override the default User model by providing a value for +the :setting:`AUTH_USER_MODEL` setting that references a custom model:: + + AUTH_USER_MODEL = 'myapp.MyUser' + +This dotted pair describes the name of the Django app, and the name of the Django +model that you wish to use as your User model. + +.. admonition:: Warning + + Changing :setting:`AUTH_USER_MODEL` has a big effect on your database + structure. It changes the tables that are available, and it will affect the + construction of foreign keys and many-to-many relationships. If you intend + to set :setting:`AUTH_USER_MODEL`, you should set it before running + ``manage.py syncdb`` for the first time. + + If you have an existing project and you want to migrate to using a custom + User model, you may need to look into using a migration tool like South_ + to ease the transition. + +.. _South: http://south.aeracode.org + +Referencing the User model +-------------------------- + +.. currentmodule:: django.contrib.auth + +If you reference :class:`~django.contrib.auth.models.User` directly (for +example, by referring to it in a foreign key), your code will not work in +projects where the :setting:`AUTH_USER_MODEL` setting has been changed to a +different User model. + +.. function:: get_user_model() + + Instead of referring to :class:`~django.contrib.auth.models.User` directly, + you should reference the user model using + ``django.contrib.auth.get_user_model()``. This method will return the + currently active User model -- the custom User model if one is specified, or + :class:`~django.contrib.auth.models.User` otherwise. + + When you define a foreign key or many-to-many relations to the User model, + you should specify the custom model using the :setting:`AUTH_USER_MODEL` + setting. For example:: + + + from django.conf import settings + from django.db import models + + class Article(models.Model) + author = models.ForeignKey(settings.AUTH_USER_MODEL) + +Specifying a custom User model +------------------------------ + +.. admonition:: Model design considerations + + Think carefully before handling information not directly related to + authentication in your custom User Model. + + It may be better to store app-specific user information in a model + that has a relation with the User model. That allows each app to specify + its own user data requirements without risking conflicts with other + apps. On the other hand, queries to retrieve this related information + will involve a database join, which may have an effect on performance. + +Django expects your custom User model to meet some minimum requirements. + +1. Your model must have a single unique field that can be used for + identification purposes. This can be a username, an email address, + or any other unique attribute. + +2. Your model must provide a way to address the user in a "short" and + "long" form. The most common interpretation of this would be to use + the user's given name as the "short" identifier, and the user's full + name as the "long" identifier. However, there are no constraints on + what these two methods return - if you want, they can return exactly + the same value. + +The easiest way to construct a compliant custom User model is to inherit from +:class:`~django.contrib.auth.models.AbstractBaseUser`. +:class:`~django.contrib.auth.models.AbstractBaseUser` provides the core +implementation of a `User` model, including hashed passwords and tokenized +password resets. You must then provide some key implementation details: + +.. currentmodule:: django.contrib.auth + +.. class:: models.CustomUser + + .. attribute:: USERNAME_FIELD + + A string describing the name of the field on the User model that is + used as the unique identifier. This will usually be a username of + some kind, but it can also be an email address, or any other unique + identifier. In the following example, the field `identifier` is used + as the identifying field:: + + class MyUser(AbstractBaseUser): + identifier = models.CharField(max_length=40, unique=True, db_index=True) + ... + USERNAME_FIELD = 'identifier' + + .. attribute:: REQUIRED_FIELDS + + A list of the field names that *must* be provided when creating + a user. For example, here is the partial definition for a User model + that defines two required fields - a date of birth and height:: + + class MyUser(AbstractBaseUser): + ... + date_of_birth = models.DateField() + height = models.FloatField() + ... + REQUIRED_FIELDS = ['date_of_birth', 'height'] + + .. note:: + + ``REQUIRED_FIELDS`` must contain all required fields on your User + model, but should *not* contain the ``USERNAME_FIELD``. + + .. attribute:: is_active + + A boolean attribute that indicates whether the user is considered + "active". This attribute is provided as an attribute on + ``AbstractBaseUser`` defaulting to ``True``. How you choose to + implement it will depend on the details of your chosen auth backends. + See the documentation of the :attr:`attribute on the builtin user model + ` for details. + + .. method:: get_full_name() + + A longer formal identifier for the user. A common interpretation + would be the full name name of the user, but it can be any string that + identifies the user. + + .. method:: get_short_name() + + A short, informal identifier for the user. A common interpretation + would be the first name of the user, but it can be any string that + identifies the user in an informal way. It may also return the same + value as :meth:`django.contrib.auth.models.User.get_full_name()`. + +The following methods are available on any subclass of +:class:`~django.contrib.auth.models.AbstractBaseUser`: + +.. class:: models.AbstractBaseUser + + .. method:: get_username() + + Returns the value of the field nominated by ``USERNAME_FIELD``. + + .. method:: models.AbstractBaseUser.is_anonymous() + + Always returns ``False``. This is a way of differentiating + from :class:`~django.contrib.auth.models.AnonymousUser` objects. + Generally, you should prefer using + :meth:`~django.contrib.auth.models.AbstractBaseUser.is_authenticated()` to this + method. + + .. method:: models.AbstractBaseUser.is_authenticated() + + Always returns ``True``. This is a way to tell if the user has been + authenticated. This does not imply any permissions, and doesn't check + if the user is active - it only indicates that the user has provided a + valid username and password. + + .. method:: models.AbstractBaseUser.set_password(raw_password) + + Sets the user's password to the given raw string, taking care of the + password hashing. Doesn't save the + :class:`~django.contrib.auth.models.AbstractBaseUser` object. + + .. method:: models.AbstractBaseUser.check_password(raw_password) + + Returns ``True`` if the given raw string is the correct password for + the user. (This takes care of the password hashing in making the + comparison.) + + .. method:: models.AbstractBaseUser.set_unusable_password() + + Marks the user as having no password set. This isn't the same as + having a blank string for a password. + :meth:`~django.contrib.auth.models.AbstractBaseUser.check_password()` for this user + will never return ``True``. Doesn't save the + :class:`~django.contrib.auth.models.AbstractBaseUser` object. + + You may need this if authentication for your application takes place + against an existing external source such as an LDAP directory. + + .. method:: models.AbstractBaseUser.has_usable_password() + + Returns ``False`` if + :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()` has + been called for this user. + +You should also define a custom manager for your User model. If your User +model defines `username` and `email` fields the same as Django's default User, +you can just install Django's +:class:`~django.contrib.auth.models.UserManager`; however, if your User model +defines different fields, you will need to define a custom manager that +extends :class:`~django.contrib.auth.models.BaseUserManager` providing two +additional methods: + +.. class:: models.CustomUserManager + + .. method:: models.CustomUserManager.create_user(*username_field*, password=None, \**other_fields) + + The prototype of `create_user()` should accept the username field, + plus all required fields as arguments. For example, if your user model + uses `email` as the username field, and has `date_of_birth` as a required + fields, then create_user should be defined as:: + + def create_user(self, email, date_of_birth, password=None): + # create user here + + .. method:: models.CustomUserManager.create_superuser(*username_field*, password, \**other_fields) + + The prototype of `create_superuser()` should accept the username field, + plus all required fields as arguments. For example, if your user model + uses `email` as the username field, and has `date_of_birth` as a required + fields, then create_superuser should be defined as:: + + def create_superuser(self, email, date_of_birth, password): + # create superuser here + + Unlike `create_user()`, `create_superuser()` *must* require the caller + to provider a password. + +:class:`~django.contrib.auth.models.BaseUserManager` provides the following +utility methods: + +.. class:: models.BaseUserManager + + .. method:: models.BaseUserManager.normalize_email(email) + + A classmethod that normalizes email addresses by lowercasing + the domain portion of the email address. + + .. method:: models.BaseUserManager.get_by_natural_key(username) + + Retrieves a user instance using the contents of the field + nominated by ``USERNAME_FIELD``. + + .. method:: models.BaseUserManager.make_random_password(length=10, allowed_chars='abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789') + + Returns a random password with the given length and given string of + allowed characters. (Note that the default value of ``allowed_chars`` + doesn't contain letters that can cause user confusion, including: + + * ``i``, ``l``, ``I``, and ``1`` (lowercase letter i, lowercase + letter L, uppercase letter i, and the number one) + * ``o``, ``O``, and ``0`` (uppercase letter o, lowercase letter o, + and zero) + +Extending Django's default User +------------------------------- + +If you're entirely happy with Django's :class:`~django.contrib.auth.models.User` +model and you just want to add some additional profile information, you can +simply subclass ``django.contrib.auth.models.AbstractUser`` and add your +custom profile fields. This class provides the full implementation of the +default :class:`~django.contrib.auth.models.User` as an :ref:`abstract model +`. + +Custom users and the built-in auth forms +---------------------------------------- + +As you may expect, built-in Django's :ref:`forms ` and +:ref:`views ` make certain assumptions about the user +model that they are working with. + +If your user model doesn't follow the same assumptions, it may be necessary to define +a replacement form, and pass that form in as part of the configuration of the +auth views. + +* :class:`~django.contrib.auth.forms.UserCreationForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.UserChangeForm` + + Depends on the :class:`~django.contrib.auth.models.User` model. + Must be re-written for any custom user model. + +* :class:`~django.contrib.auth.forms.AuthenticationForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`, + and will adapt to use the field defined in `USERNAME_FIELD`. + +* :class:`~django.contrib.auth.forms.PasswordResetForm` + + Assumes that the user model has an integer primary key, has a field named + `email` that can be used to identify the user, and a boolean field + named `is_active` to prevent password resets for inactive users. + +* :class:`~django.contrib.auth.forms.SetPasswordForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.PasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + +* :class:`~django.contrib.auth.forms.AdminPasswordChangeForm` + + Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` + + +Custom users and django.contrib.admin +------------------------------------- + +If you want your custom User model to also work with Admin, your User model must +define some additional attributes and methods. These methods allow the admin to +control access of the User to admin content: + +.. class:: models.CustomUser + +.. attribute:: is_staff + + Returns True if the user is allowed to have access to the admin site. + +.. attribute:: is_active + + Returns True if the user account is currently active. + +.. method:: has_perm(perm, obj=None): + + Returns True if the user has the named permission. If `obj` is + provided, the permission needs to be checked against a specific object + instance. + +.. method:: has_module_perms(app_label): + + Returns True if the user has permission to access models in + the given app. + +You will also need to register your custom User model with the admin. If +your custom User model extends ``django.contrib.auth.models.AbstractUser``, +you can use Django's existing ``django.contrib.auth.admin.UserAdmin`` +class. However, if your User model extends +:class:`~django.contrib.auth.models.AbstractBaseUser`, you'll need to define +a custom ModelAdmin class. It may be possible to subclass the default +``django.contrib.auth.admin.UserAdmin``; however, you'll need to +override any of the definitions that refer to fields on +``django.contrib.auth.models.AbstractUser`` that aren't on your +custom User class. + +Custom users and permissions +---------------------------- + +To make it easy to include Django's permission framework into your own User +class, Django provides :class:`~django.contrib.auth.models.PermissionsMixin`. +This is an abstract model you can include in the class hierarchy for your User +model, giving you all the methods and database fields necessary to support +Django's permission model. + +:class:`~django.contrib.auth.models.PermissionsMixin` provides the following +methods and attributes: + +.. class:: models.PermissionsMixin + + .. attribute:: models.PermissionsMixin.is_superuser + + Boolean. Designates that this user has all permissions without + explicitly assigning them. + + .. method:: models.PermissionsMixin.get_group_permissions(obj=None) + + Returns a set of permission strings that the user has, through his/her + groups. + + If ``obj`` is passed in, only returns the group permissions for + this specific object. + + .. method:: models.PermissionsMixin.get_all_permissions(obj=None) + + Returns a set of permission strings that the user has, both through + group and user permissions. + + If ``obj`` is passed in, only returns the permissions for this + specific object. + + .. method:: models.PermissionsMixin.has_perm(perm, obj=None) + + Returns ``True`` if the user has the specified permission, where perm is + in the format ``"."`` (see + :ref:`permissions `). If the user is inactive, this method will + always return ``False``. + + If ``obj`` is passed in, this method won't check for a permission for + the model, but for this specific object. + + .. method:: models.PermissionsMixin.has_perms(perm_list, obj=None) + + Returns ``True`` if the user has each of the specified permissions, + where each perm is in the format + ``"."``. If the user is inactive, + this method will always return ``False``. + + If ``obj`` is passed in, this method won't check for permissions for + the model, but for the specific object. + + .. method:: models.PermissionsMixin.has_module_perms(package_name) + + Returns ``True`` if the user has any permissions in the given package + (the Django app label). If the user is inactive, this method will + always return ``False``. + +.. admonition:: ModelBackend + + If you don't include the + :class:`~django.contrib.auth.models.PermissionsMixin`, you must ensure you + don't invoke the permissions methods on ``ModelBackend``. ``ModelBackend`` + assumes that certain fields are available on your user model. If your User + model doesn't provide those fields, you will receive database errors when + you check permissions. + +Custom users and Proxy models +----------------------------- + +One limitation of custom User models is that installing a custom User model +will break any proxy model extending :class:`~django.contrib.auth.models.User`. +Proxy models must be based on a concrete base class; by defining a custom User +model, you remove the ability of Django to reliably identify the base class. + +If your project uses proxy models, you must either modify the proxy to extend +the User model that is currently in use in your project, or merge your proxy's +behavior into your User subclass. + +Custom users and signals +------------------------ + +Another limitation of custom User models is that you can't use +:func:`django.contrib.auth.get_user_model()` as the sender or target of a signal +handler. Instead, you must register the handler with the resulting User model. +See :doc:`/topics/signals` for more information on registering an sending +signals. + +Custom users and testing/fixtures +--------------------------------- + +If you are writing an application that interacts with the User model, you must +take some precautions to ensure that your test suite will run regardless of +the User model that is being used by a project. Any test that instantiates an +instance of User will fail if the User model has been swapped out. This +includes any attempt to create an instance of User with a fixture. + +To ensure that your test suite will pass in any project configuration, +``django.contrib.auth.tests.utils`` defines a ``@skipIfCustomUser`` decorator. +This decorator will cause a test case to be skipped if any User model other +than the default Django user is in use. This decorator can be applied to a +single test, or to an entire test class. + +Depending on your application, tests may also be needed to be added to ensure +that the application works with *any* user model, not just the default User +model. To assist with this, Django provides two substitute user models that +can be used in test suites: + +* ``django.contrib.auth.tests.custom_user.CustomUser``, a custom user + model that uses an ``email`` field as the username, and has a basic + admin-compliant permissions setup + +* ``django.contrib.auth.tests.custom_user.ExtensionUser``, a custom + user model that extends ``django.contrib.auth.models.AbstractUser``, + adding a ``date_of_birth`` field. + +You can then use the ``@override_settings`` decorator to make that test run +with the custom User model. For example, here is a skeleton for a test that +would test three possible User models -- the default, plus the two User +models provided by ``auth`` app:: + + from django.contrib.auth.tests.utils import skipIfCustomUser + from django.test import TestCase + from django.test.utils import override_settings + + + class ApplicationTestCase(TestCase): + @skipIfCustomUser + def test_normal_user(self): + "Run tests for the normal user model" + self.assertSomething() + + @override_settings(AUTH_USER_MODEL='auth.CustomUser') + def test_custom_user(self): + "Run tests for a custom user model with email-based authentication" + self.assertSomething() + + @override_settings(AUTH_USER_MODEL='auth.ExtensionUser') + def test_extension_user(self): + "Run tests for a simple extension of the built-in User." + self.assertSomething() + + +A full example +-------------- + +Here is an example of an admin-compliant custom user app. This user model uses +an email address as the username, and has a required date of birth; it +provides no permission checking, beyond a simple `admin` flag on the user +account. This model would be compatible with all the built-in auth forms and +views, except for the User creation forms. + +This code would all live in a ``models.py`` file for a custom +authentication app:: + + from django.db import models + from django.contrib.auth.models import ( + BaseUserManager, AbstractBaseUser + ) + + + class MyUserManager(BaseUserManager): + def create_user(self, email, date_of_birth, password=None): + """ + Creates and saves a User with the given email, date of + birth and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model( + email=MyUserManager.normalize_email(email), + date_of_birth=date_of_birth, + ) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, date_of_birth, password): + """ + Creates and saves a superuser with the given email, date of + birth and password. + """ + user = self.create_user(email, + password=password, + date_of_birth=date_of_birth + ) + user.is_admin = True + user.save(using=self._db) + return user + + + class MyUser(AbstractBaseUser): + email = models.EmailField( + verbose_name='email address', + max_length=255, + unique=True, + db_index=True, + ) + date_of_birth = models.DateField() + is_active = models.BooleanField(default=True) + is_admin = models.BooleanField(default=False) + + objects = MyUserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['date_of_birth'] + + def get_full_name(self): + # The user is identified by their email address + return self.email + + def get_short_name(self): + # The user is identified by their email address + return self.email + + def __unicode__(self): + return self.email + + def has_perm(self, perm, obj=None): + "Does the user have a specific permission?" + # Simplest possible answer: Yes, always + return True + + def has_module_perms(self, app_label): + "Does the user have permissions to view the app `app_label`?" + # Simplest possible answer: Yes, always + return True + + @property + def is_staff(self): + "Is the user a member of staff?" + # Simplest possible answer: All admins are staff + return self.is_admin + +Then, to register this custom User model with Django's admin, the following +code would be required in the app's ``admin.py`` file:: + + from django import forms + from django.contrib import admin + from django.contrib.auth.models import Group + from django.contrib.auth.admin import UserAdmin + from django.contrib.auth.forms import ReadOnlyPasswordHashField + + from customauth.models import MyUser + + + class UserCreationForm(forms.ModelForm): + """A form for creating new users. Includes all the required + fields, plus a repeated password.""" + password1 = forms.CharField(label='Password', widget=forms.PasswordInput) + password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput) + + class Meta: + model = MyUser + fields = ('email', 'date_of_birth') + + def clean_password2(self): + # Check that the two password entries match + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError("Passwords don't match") + return password2 + + def save(self, commit=True): + # Save the provided password in hashed format + user = super(UserCreationForm, self).save(commit=False) + user.set_password(self.cleaned_data["password1"]) + if commit: + user.save() + return user + + + class UserChangeForm(forms.ModelForm): + """A form for updating users. Includes all the fields on + the user, but replaces the password field with admin's + password hash display field. + """ + password = ReadOnlyPasswordHashField() + + class Meta: + model = MyUser + + def clean_password(self): + # Regardless of what the user provides, return the initial value. + # This is done here, rather than on the field, because the + # field does not have access to the initial value + return self.initial["password"] + + + class MyUserAdmin(UserAdmin): + # The forms to add and change user instances + form = UserChangeForm + add_form = UserCreationForm + + # The fields to be used in displaying the User model. + # These override the definitions on the base UserAdmin + # that reference specific fields on auth.User. + list_display = ('email', 'date_of_birth', 'is_admin') + list_filter = ('is_admin',) + fieldsets = ( + (None, {'fields': ('email', 'password')}), + ('Personal info', {'fields': ('date_of_birth',)}), + ('Permissions', {'fields': ('is_admin',)}), + ('Important dates', {'fields': ('last_login',)}), + ) + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'date_of_birth', 'password1', 'password2')} + ), + ) + search_fields = ('email',) + ordering = ('email',) + filter_horizontal = () + + # Now register the new UserAdmin... + admin.site.register(MyUser, MyUserAdmin) + # ... and, since we're not using Django's builtin permissions, + # unregister the Group model from admin. + admin.site.unregister(Group) diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt new file mode 100644 index 0000000000..c4736135b0 --- /dev/null +++ b/docs/topics/auth/default.txt @@ -0,0 +1,1077 @@ +====================================== +Using the Django authentication system +====================================== + +.. currentmodule:: django.contrib.auth + +This document explains the usage of Django's authentication system in its +default configuration. This configuration has evolved to serve the most common +project needs, handling a reasonably wide range of tasks, and has a careful +implementation of passwords and permissions, and can handle many projects as +is. For projects where authentication needs differ from the default, Django +supports extensive :doc:`extension and customization +` of authentication. + +Django authentication provides both authentication and authorization, together +and is generally referred to as the authentication system, as these features +somewhat coupled. + +.. _user-objects: + +User objects +============ + +:class:`~django.contrib.auth.models.User` objects are the core of the +authentication system. They typically represent the people interacting with +your site and are used to enable things like restricting access, registering +user profiles, associating content with creators etc. Only one class of user +exists in Django's authentication framework, i.e., 'superusers' or admin +'staff' users are is just a user objects with special attributes set, not +different classes of user objects. + +The primary attributes of the default user are: + +* username +* password +* email +* first name +* last name + +See the :class:`full API documentation ` for +full reference, the documentation that follows is more task oriented. + +.. _topics-auth-creating-users: + +Creating users +-------------- + +The most direct way to create users is to use the included +:meth:`~django.contrib.auth.models.UserManager.create_user` helper function:: + + >>> from django.contrib.auth.models import User + >>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword') + + # At this point, user is a User object that has already been saved + # to the database. You can continue to change its attributes + # if you want to change other fields. + >>> user.last_name = 'Lennon' + >>> user.save() + +If you have the Django admin installed, you can also :ref:`create users +interactively `. + +.. _topics-auth-creating-superusers: + +Creating superusers +------------------- + +:djadmin:`manage.py syncdb ` prompts you to create a superuser the +first time you run it with ``'django.contrib.auth'`` in your +:setting:`INSTALLED_APPS`. If you need to create a superuser at a later date, +you can use a command line utility:: + + manage.py createsuperuser --username=joe --email=joe@example.com + +You will be prompted for a password. After you enter one, the user will be +created immediately. If you leave off the :djadminopt:`--username` or the +:djadminopt:`--email` options, it will prompt you for those values. + +Changing passwords +------------------ + +Django does not store raw (clear text) passwords on the user model, but only +a hash (see :doc:`documentation of how passwords are managed +` for full details). Because of this, do not attempt to +manipulate the password attribute of the user directly. This is why a a helper +function is used when creating a user. + +To change a user's password, you have several options: + +:djadmin:`manage.py changepassword *username* ` offers a method +of changing a User's password from the command line. It prompts you to +change the password of a given user which you must enter twice. If +they both match, the new password will be changed immediately. If you +do not supply a user, the command will attempt to change the password +whose username matches the current system user. + +You can also change a password programmatically, using +:meth:`~django.contrib.auth.models.User.set_password()`: + +.. code-block:: python + + >>> from django.contrib.auth.models import User + >>> u = User.objects.get(username__exact='john') + >>> u.set_password('new password') + >>> u.save() + +If you have the Django admin installed, you can also change user's passwords +on the :ref:`authentication system's admin pages `. + +Django also provides :ref:`views ` and :ref:`forms +` that may be used to allow users to change their own +passwords. + +Authenticating Users +-------------------- + +.. function:: authenticate(\**credentials) + + To authenticate a given username and password, use + :func:`~django.contrib.auth.authenticate()`. It takes credentials in the + form of keyword arguments, for the default configuration this is + ``username`` and ``password``, and it returns + a :class:`~django.contrib.auth.models.User` object if the password is valid + for the given username. If the password is invalid, + :func:`~django.contrib.auth.authenticate()` returns ``None``. Example:: + + from django.contrib.auth import authenticate + user = authenticate(username='john', password='secret') + if user is not None: + # the password verified for the user + if user.is_active: + print("User is valid, active and authenticated") + else: + print("The password is valid, but the account has been disabled!") + else: + # the authentication system was unable to verify the username and password + print("The username and password were incorrect.") + +.. _topic-authorization: + +Permissions and Authorization +============================= + +Django comes with a simple permissions system. It provides a way to assign +permissions to specific users and groups of users. + +It's used by the Django admin site, but you're welcome to use it in your own +code. + +The Django admin site uses permissions as follows: + +* Access to view the "add" form and add an object is limited to users with + the "add" permission for that type of object. +* Access to view the change list, view the "change" form and change an + object is limited to users with the "change" permission for that type of + object. +* Access to delete an object is limited to users with the "delete" + permission for that type of object. + +Permissions can be set not only per type of object, but also per specific +object instance. By using the +:meth:`~django.contrib.admin.ModelAdmin.has_add_permission`, +:meth:`~django.contrib.admin.ModelAdmin.has_change_permission` and +:meth:`~django.contrib.admin.ModelAdmin.has_delete_permission` methods provided +by the :class:`~django.contrib.admin.ModelAdmin` class, it is possible to +customize permissions for different object instances of the same type. + +:class:`~django.contrib.auth.models.User` objects have two many-to-many +fields: ``groups`` and ``user_permissions``. +:class:`~django.contrib.auth.models.User` objects can access their related +objects in the same way as any other :doc:`Django model +`: + +.. code-block:: python + + myuser.groups = [group_list] + myuser.groups.add(group, group, ...) + myuser.groups.remove(group, group, ...) + myuser.groups.clear() + myuser.user_permissions = [permission_list] + myuser.user_permissions.add(permission, permission, ...) + myuser.user_permissions.remove(permission, permission, ...) + myuser.user_permissions.clear() + +Default permissions +------------------- + +When ``django.contrib.auth`` is listed in your :setting:`INSTALLED_APPS` +setting, it will ensure that three default permissions -- add, change and +delete -- are created for each Django model defined in one of your installed +applications. + +These permissions will be created when you run :djadmin:`manage.py syncdb +`; the first time you run ``syncdb`` after adding +``django.contrib.auth`` to :setting:`INSTALLED_APPS`, the default permissions +will be created for all previously-installed models, as well as for any new +models being installed at that time. Afterward, it will create default +permissions for new models each time you run :djadmin:`manage.py syncdb +`. + +Assuming you have an application with an +:attr:`~django.db.models.Options.app_label` ``foo`` and a model named ``Bar``, +to test for basic permissions you should use: + +* add: ``user.has_perm('foo.add_bar')`` +* change: ``user.has_perm('foo.change_bar')`` +* delete: ``user.has_perm('foo.delete_bar')`` + +The :class:`~django.contrib.auth.models.Permission` model is rarely accessed +directly. + +Groups +------ + +:class:`django.contrib.auth.models.Group` models are a generic way of +categorizing users so you can apply permissions, or some other label, to those +users. A user can belong to any number of groups. + +A user in a group automatically has the permissions granted to that group. For +example, if the group ``Site editors`` has the permission +``can_edit_home_page``, any user in that group will have that permission. + +Beyond permissions, groups are a convenient way to categorize users to give +them some label, or extended functionality. For example, you could create a +group ``'Special users'``, and you could write code that could, say, give them +access to a members-only portion of your site, or send them members-only email +messages. + +Programmatically creating permissions +------------------------------------- + +While :ref:`custom permissions ` can be defined within +a model's ``Meta`` class, you can also create permissions directly. For +example, you can create the ``can_publish`` permission for a ``BlogPost`` model +in ``myapp``:: + + from django.contrib.auth.models import Group, Permission + from django.contrib.contenttypes.models import ContentType + + content_type = ContentType.objects.get(app_label='myapp', model='BlogPost') + permission = Permission.objects.create(codename='can_publish', + name='Can Publish Posts', + content_type=content_type) + +The permission can then be assigned to a +:class:`~django.contrib.auth.models.User` via its ``user_permissions`` +attribute or to a :class:`~django.contrib.auth.models.Group` via its +``permissions`` attribute. + +.. _auth-web-requests: + +Authentication in Web requests +============================== + +Django uses :doc:`sessions ` and middleware to hook the +authentication system into :class:`request objects `. + +These provide a :attr:`request.user ` attribute +on every request which represents the current user. If the current user has not +logged in, this attribute will be set to an instance +of :class:`~django.contrib.auth.models.AnonymousUser`, otherwise it will be an +instance of :class:`~django.contrib.auth.models.User`. + +You can tell them apart with +:meth:`~django.contrib.auth.models.User.is_authenticated()`, like so:: + + if request.user.is_authenticated(): + # Do something for authenticated users. + else: + # Do something for anonymous users. + +.. _how-to-log-a-user-in: + +How to log a user in +-------------------- + +If you have an authenticated user you want to attach to the current session +- this is done with a :func:`~django.contrib.auth.login` function. + +.. function:: login() + + To log a user in, from a view, use :func:`~django.contrib.auth.login()`. It + takes an :class:`~django.http.HttpRequest` object and a + :class:`~django.contrib.auth.models.User` object. + :func:`~django.contrib.auth.login()` saves the user's ID in the session, + using Django's session framework. + + Note that any data set during the anonymous session is retained in the + session after a user logs in. + + This example shows how you might use both + :func:`~django.contrib.auth.authenticate()` and + :func:`~django.contrib.auth.login()`:: + + from django.contrib.auth import authenticate, login + + def my_view(request): + username = request.POST['username'] + password = request.POST['password'] + user = authenticate(username=username, password=password) + if user is not None: + if user.is_active: + login(request, user) + # Redirect to a success page. + else: + # Return a 'disabled account' error message + else: + # Return an 'invalid login' error message. + +.. admonition:: Calling ``authenticate()`` first + + When you're manually logging a user in, you *must* call + :func:`~django.contrib.auth.authenticate()` before you call + :func:`~django.contrib.auth.login()`. + :func:`~django.contrib.auth.authenticate()` + sets an attribute on the :class:`~django.contrib.auth.models.User` noting + which authentication backend successfully authenticated that user (see the + :ref:`backends documentation ` for details), and + this information is needed later during the login process. An error will be + raise if you try to login a user object retrieved from the database + directly. + +How to log a user out +--------------------- + +.. function:: logout() + + To log out a user who has been logged in via + :func:`django.contrib.auth.login()`, use + :func:`django.contrib.auth.logout()` within your view. It takes an + :class:`~django.http.HttpRequest` object and has no return value. + Example:: + + from django.contrib.auth import logout + + def logout_view(request): + logout(request) + # Redirect to a success page. + + Note that :func:`~django.contrib.auth.logout()` doesn't throw any errors if + the user wasn't logged in. + + When you call :func:`~django.contrib.auth.logout()`, the session data for + the current request is completely cleaned out. All existing data is + removed. This is to prevent another person from using the same Web browser + to log in and have access to the previous user's session data. If you want + to put anything into the session that will be available to the user + immediately after logging out, do that *after* calling + :func:`django.contrib.auth.logout()`. + +Limiting access to logged-in users +---------------------------------- + +The raw way +~~~~~~~~~~~ + +The simple, raw way to limit access to pages is to check +:meth:`request.user.is_authenticated() +` and either redirect to a +login page:: + + from django.shortcuts import redirect + + def my_view(request): + if not request.user.is_authenticated(): + return redirect('/login/?next=%s' % request.path) + # ... + +...or display an error message:: + + from django.shortcuts import render + + def my_view(request): + if not request.user.is_authenticated(): + return render('myapp/login_error.html') + # ... + +.. currentmodule:: django.contrib.auth.decorators + +The login_required decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. function:: login_required([redirect_field_name=REDIRECT_FIELD_NAME, login_url=None]) + + As a shortcut, you can use the convenient + :func:`~django.contrib.auth.decorators.login_required` decorator:: + + from django.contrib.auth.decorators import login_required + + @login_required + def my_view(request): + ... + + :func:`~django.contrib.auth.decorators.login_required` does the following: + + * If the user isn't logged in, redirect to + :setting:`settings.LOGIN_URL `, passing the current absolute + path in the query string. Example: ``/accounts/login/?next=/polls/3/``. + + * If the user is logged in, execute the view normally. The view code is + free to assume the user is logged in. + + By default, the path that the user should be redirected to upon + successful authentication is stored in a query string parameter called + ``"next"``. If you would prefer to use a different name for this parameter, + :func:`~django.contrib.auth.decorators.login_required` takes an + optional ``redirect_field_name`` parameter:: + + from django.contrib.auth.decorators import login_required + + @login_required(redirect_field_name='my_redirect_field') + def my_view(request): + ... + + Note that if you provide a value to ``redirect_field_name``, you will most + likely need to customize your login template as well, since the template + context variable which stores the redirect path will use the value of + ``redirect_field_name`` as its key rather than ``"next"`` (the default). + + :func:`~django.contrib.auth.decorators.login_required` also takes an + optional ``login_url`` parameter. Example:: + + from django.contrib.auth.decorators import login_required + + @login_required(login_url='/accounts/login/') + def my_view(request): + ... + + Note that if you don't specify the ``login_url`` parameter, you'll need to + ensure that the :setting:`settings.LOGIN_URL ` and your login + view are properly associated. For example, using the defaults, add the + following line to your URLconf:: + + (r'^accounts/login/$', 'django.contrib.auth.views.login'), + + .. versionchanged:: 1.5 + + The :setting:`settings.LOGIN_URL ` also accepts + view function names and :ref:`named URL patterns `. + This allows you to freely remap your login view within your URLconf + without having to update the setting. + +.. note:: + + The login_required decorator does NOT check the is_active flag on a user. + +Limiting access to logged-in users that pass a test +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To limit access based on certain permissions or some other test, you'd do +essentially the same thing as described in the previous section. + +The simple way is to run your test on :attr:`request.user +` in the view directly. For example, this view +checks to make sure the user has an email in the desired domain:: + + def my_view(request): + if not '@example.com' in request.user.email: + return HttpResponse("You can't vote in this poll.") + # ... + +.. function:: user_passes_test(func, [login_url=None]) + + As a shortcut, you can use the convenient ``user_passes_test`` decorator:: + + from django.contrib.auth.decorators import user_passes_test + + def email_check(user): + return '@example.com' in request.user.email + + @user_passes_test(email_check) + def my_view(request): + ... + + :func:`~django.contrib.auth.decorators.user_passes_test` takes a required + argument: a callable that takes a + :class:`~django.contrib.auth.models.User` object and returns ``True`` if + the user is allowed to view the page. Note that + :func:`~django.contrib.auth.decorators.user_passes_test` does not + automatically check that the :class:`~django.contrib.auth.models.User` is + not anonymous. + + :func:`~django.contrib.auth.decorators.user_passes_test()` takes an + optional ``login_url`` argument, which lets you specify the URL for your + login page (:setting:`settings.LOGIN_URL ` by default). + + For example:: + + @user_passes_test(email_check, login_url='/login/') + def my_view(request): + ... + +The permission_required decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. function:: permission_required([login_url=None, raise_exception=False]) + + It's a relatively common task to check whether a user has a particular + permission. For that reason, Django provides a shortcut for that case: the + :func:`~django.contrib.auth.decorators.permission_required()` decorator.:: + + from django.contrib.auth.decorators import permission_required + + @permission_required('polls.can_vote') + def my_view(request): + ... + + As for the :meth:`~django.contrib.auth.models.User.has_perm` method, + permission names take the form ``"."`` + (i.e. ``polls.can_vote`` for a permission on a model in the ``polls`` + application). + + Note that :func:`~django.contrib.auth.decorators.permission_required()` + also takes an optional ``login_url`` parameter. Example:: + + from django.contrib.auth.decorators import permission_required + + @permission_required('polls.can_vote', login_url='/loginpage/') + def my_view(request): + ... + + As in the :func:`~django.contrib.auth.decorators.login_required` decorator, + ``login_url`` defaults to :setting:`settings.LOGIN_URL `. + + .. versionchanged:: 1.4 + + Added ``raise_exception`` parameter. If given, the decorator will raise + :exc:`~django.core.exceptions.PermissionDenied`, prompting + :ref:`the 403 (HTTP Forbidden) view` instead of + redirecting to the login page. + +Applying permissions to generic views +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To apply a permission to a :doc:`class-based generic view +`, decorate the :meth:`View.dispatch +` method on the class. See +:ref:`decorating-class-based-views` for details. + + +.. _built-in-auth-views: + +Authentication Views +-------------------- + +.. module:: django.contrib.auth.views + +Django provides several views that you can use for handling login, logout, and +password management. These make use of the :ref:`stock auth forms +` but you can pass in your own forms as well. + +Django provides no default template for the authentication views - however the +template context is documented for each view below. + +.. versionadded:: 1.4 + +The built-in views all return +a :class:`~django.template.response.TemplateResponse` instance, which allows +you to easily customize the response data before rendering. For more details, +see the :doc:`TemplateResponse documentation `. + +Most built-in authentication views provide a URL name for easier reference. See +:doc:`the URL documentation ` for details on using named URL +patterns. + + +.. function:: login(request, [template_name, redirect_field_name, authentication_form]) + + **URL name:** ``login`` + + See :doc:`the URL documentation ` for details on using + named URL patterns. + + Here's what ``django.contrib.auth.views.login`` does: + + * If called via ``GET``, it displays a login form that POSTs to the + same URL. More on this in a bit. + + * If called via ``POST`` with user submitted credentials, it tries to log + the user in. If login is successful, the view redirects to the URL + specified in ``next``. If ``next`` isn't provided, it redirects to + :setting:`settings.LOGIN_REDIRECT_URL ` (which + defaults to ``/accounts/profile/``). If login isn't successful, it + redisplays the login form. + + It's your responsibility to provide the html for the login template + , called ``registration/login.html`` by default. This template gets passed + four template context variables: + + * ``form``: A :class:`~django.forms.Form` object representing the + :class:`~django.contrib.auth.forms.AuthenticationForm`. + + * ``next``: The URL to redirect to after successful login. This may + contain a query string, too. + + * ``site``: The current :class:`~django.contrib.sites.models.Site`, + according to the :setting:`SITE_ID` setting. If you don't have the + site framework installed, this will be set to an instance of + :class:`~django.contrib.sites.models.RequestSite`, which derives the + site name and domain from the current + :class:`~django.http.HttpRequest`. + + * ``site_name``: An alias for ``site.name``. If you don't have the site + framework installed, this will be set to the value of + :attr:`request.META['SERVER_NAME'] `. + For more on sites, see :doc:`/ref/contrib/sites`. + + If you'd prefer not to call the template :file:`registration/login.html`, + you can pass the ``template_name`` parameter via the extra arguments to + the view in your URLconf. For example, this URLconf line would use + :file:`myapp/login.html` instead:: + + (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'myapp/login.html'}), + + You can also specify the name of the ``GET`` field which contains the URL + to redirect to after login by passing ``redirect_field_name`` to the view. + By default, the field is called ``next``. + + Here's a sample :file:`registration/login.html` template you can use as a + starting point. It assumes you have a :file:`base.html` template that + defines a ``content`` block: + + .. code-block:: html+django + + {% extends "base.html" %} + + {% block content %} + + {% if form.errors %} +

    Your username and password didn't match. Please try again.

    + {% endif %} + +
    + {% csrf_token %} + + + + + + + + + +
    {{ form.username.label_tag }}{{ form.username }}
    {{ form.password.label_tag }}{{ form.password }}
    + + + +
    + + {% endblock %} + + If you have customized authentication (see + :doc:`Customizing Authentication `) you can pass a custom authentication form + to the login view via the ``authentication_form`` parameter. This form must + accept a ``request`` keyword argument in its ``__init__`` method, and + provide a ``get_user`` method which returns the authenticated user object + (this method is only ever called after successful form validation). + + .. _forms documentation: ../forms/ + .. _site framework docs: ../sites/ + + +.. function:: logout(request, [next_page, template_name, redirect_field_name]) + + Logs a user out. + + **URL name:** ``logout`` + + **Optional arguments:** + + * ``next_page``: The URL to redirect to after logout. + + * ``template_name``: The full name of a template to display after + logging the user out. Defaults to + :file:`registration/logged_out.html` if no argument is supplied. + + * ``redirect_field_name``: The name of a ``GET`` field containing the + URL to redirect to after log out. Overrides ``next_page`` if the given + ``GET`` parameter is passed. + + **Template context:** + + * ``title``: The string "Logged out", localized. + + * ``site``: The current :class:`~django.contrib.sites.models.Site`, + according to the :setting:`SITE_ID` setting. If you don't have the + site framework installed, this will be set to an instance of + :class:`~django.contrib.sites.models.RequestSite`, which derives the + site name and domain from the current + :class:`~django.http.HttpRequest`. + + * ``site_name``: An alias for ``site.name``. If you don't have the site + framework installed, this will be set to the value of + :attr:`request.META['SERVER_NAME'] `. + For more on sites, see :doc:`/ref/contrib/sites`. + +.. function:: logout_then_login(request[, login_url]) + + Logs a user out, then redirects to the login page. + + **URL name:** No default URL provided + + **Optional arguments:** + + * ``login_url``: The URL of the login page to redirect to. + Defaults to :setting:`settings.LOGIN_URL ` if not supplied. + +.. function:: password_change(request[, template_name, post_change_redirect, password_change_form]) + + Allows a user to change their password. + + **URL name:** ``password_change`` + + **Optional arguments:** + + * ``template_name``: The full name of a template to use for + displaying the password change form. Defaults to + :file:`registration/password_change_form.html` if not supplied. + + * ``post_change_redirect``: The URL to redirect to after a successful + password change. + + * ``password_change_form``: A custom "change password" form which must + accept a ``user`` keyword argument. The form is responsible for + actually changing the user's password. Defaults to + :class:`~django.contrib.auth.forms.PasswordChangeForm`. + + **Template context:** + + * ``form``: The password change form (see ``password_change_form`` above). + +.. function:: password_change_done(request[, template_name]) + + The page shown after a user has changed their password. + + **URL name:** ``password_change_done`` + + **Optional arguments:** + + * ``template_name``: The full name of a template to use. + Defaults to :file:`registration/password_change_done.html` if not + supplied. + +.. function:: password_reset(request[, is_admin_site, template_name, email_template_name, password_reset_form, token_generator, post_reset_redirect, from_email]) + + Allows a user to reset their password by generating a one-time use link + that can be used to reset the password, and sending that link to the + user's registered email address. + + .. versionchanged:: 1.4 + Users flagged with an unusable password (see + :meth:`~django.contrib.auth.models.User.set_unusable_password()` + will not be able to request a password reset to prevent misuse + when using an external authentication source like LDAP. + + **URL name:** ``password_reset`` + + **Optional arguments:** + + * ``template_name``: The full name of a template to use for + displaying the password reset form. Defaults to + :file:`registration/password_reset_form.html` if not supplied. + + * ``email_template_name``: The full name of a template to use for + generating the email with the reset password link. Defaults to + :file:`registration/password_reset_email.html` if not supplied. + + * ``subject_template_name``: The full name of a template to use for + the subject of the email with the reset password link. Defaults + to :file:`registration/password_reset_subject.txt` if not supplied. + + .. versionadded:: 1.4 + + * ``password_reset_form``: Form that will be used to get the email of + the user to reset the password for. Defaults to + :class:`~django.contrib.auth.forms.PasswordResetForm`. + + * ``token_generator``: Instance of the class to check the one time link. + This will default to ``default_token_generator``, it's an instance of + ``django.contrib.auth.tokens.PasswordResetTokenGenerator``. + + * ``post_reset_redirect``: The URL to redirect to after a successful + password reset request. + + * ``from_email``: A valid email address. By default Django uses + the :setting:`DEFAULT_FROM_EMAIL`. + + **Template context:** + + * ``form``: The form (see ``password_reset_form`` above) for resetting + the user's password. + + **Email template context:** + + * ``email``: An alias for ``user.email`` + + * ``user``: The current :class:`~django.contrib.auth.models.User`, + according to the ``email`` form field. Only active users are able to + reset their passwords (``User.is_active is True``). + + * ``site_name``: An alias for ``site.name``. If you don't have the site + framework installed, this will be set to the value of + :attr:`request.META['SERVER_NAME'] `. + For more on sites, see :doc:`/ref/contrib/sites`. + + * ``domain``: An alias for ``site.domain``. If you don't have the site + framework installed, this will be set to the value of + ``request.get_host()``. + + * ``protocol``: http or https + + * ``uid``: The user's id encoded in base 36. + + * ``token``: Token to check that the reset link is valid. + + Sample ``registration/password_reset_email.html`` (email body template): + + .. code-block:: html+django + + Someone asked for password reset for email {{ email }}. Follow the link below: + {{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb36=uid token=token %} + + The same template context is used for subject template. Subject must be + single line plain text string. + + +.. function:: password_reset_done(request[, template_name]) + + The page shown after a user has been emailed a link to reset their + password. This view is called by default if the :func:`password_reset` view + doesn't have an explicit ``post_reset_redirect`` URL set. + + **URL name:** ``password_reset_done`` + + **Optional arguments:** + + * ``template_name``: The full name of a template to use. + Defaults to :file:`registration/password_reset_done.html` if not + supplied. + +.. function:: password_reset_confirm(request[, uidb36, token, template_name, token_generator, set_password_form, post_reset_redirect]) + + Presents a form for entering a new password. + + **URL name:** ``password_reset_confirm`` + + **Optional arguments:** + + * ``uidb36``: The user's id encoded in base 36. Defaults to ``None``. + + * ``token``: Token to check that the password is valid. Defaults to + ``None``. + + * ``template_name``: The full name of a template to display the confirm + password view. Default value is :file:`registration/password_reset_confirm.html`. + + * ``token_generator``: Instance of the class to check the password. This + will default to ``default_token_generator``, it's an instance of + ``django.contrib.auth.tokens.PasswordResetTokenGenerator``. + + * ``set_password_form``: Form that will be used to set the password. + Defaults to :class:`~django.contrib.auth.forms.SetPasswordForm` + + * ``post_reset_redirect``: URL to redirect after the password reset + done. Defaults to ``None``. + + **Template context:** + + * ``form``: The form (see ``set_password_form`` above) for setting the + new user's password. + + * ``validlink``: Boolean, True if the link (combination of uidb36 and + token) is valid or unused yet. + +.. function:: password_reset_complete(request[,template_name]) + + Presents a view which informs the user that the password has been + successfully changed. + + **URL name:** ``password_reset_complete`` + + **Optional arguments:** + + * ``template_name``: The full name of a template to display the view. + Defaults to :file:`registration/password_reset_complete.html`. + +Helper functions +---------------- + +.. currentmodule:: django.contrib.auth.views + +.. function:: redirect_to_login(next[, login_url, redirect_field_name]) + + Redirects to the login page, and then back to another URL after a + successful login. + + **Required arguments:** + + * ``next``: The URL to redirect to after a successful login. + + **Optional arguments:** + + * ``login_url``: The URL of the login page to redirect to. + Defaults to :setting:`settings.LOGIN_URL ` if not supplied. + + * ``redirect_field_name``: The name of a ``GET`` field containing the + URL to redirect to after log out. Overrides ``next`` if the given + ``GET`` parameter is passed. + + +.. _built-in-auth-forms: + +Built-in forms +-------------- + +.. module:: django.contrib.auth.forms + +If you don't want to use the built-in views, but want the convenience of not +having to write forms for this functionality, the authentication system +provides several built-in forms located in :mod:`django.contrib.auth.forms`: + +.. class:: AdminPasswordChangeForm + + A form used in the admin interface to change a user's password. + +.. class:: AuthenticationForm + + A form for logging a user in. + +.. class:: PasswordChangeForm + + A form for allowing a user to change their password. + +.. class:: PasswordResetForm + + A form for generating and emailing a one-time use link to reset a + user's password. + +.. class:: SetPasswordForm + + A form that lets a user change his/her password without entering the old + password. + +.. class:: UserChangeForm + + A form used in the admin interface to change a user's information and + permissions. + +.. class:: UserCreationForm + + A form for creating a new user. + +.. currentmodule:: django.contrib.auth + + +Authentication data in templates +-------------------------------- + +The currently logged-in user and his/her permissions are made available in the +:doc:`template context ` when you use +:class:`~django.template.RequestContext`. + +.. admonition:: Technicality + + Technically, these variables are only made available in the template context + if you use :class:`~django.template.RequestContext` *and* your + :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting contains + ``"django.contrib.auth.context_processors.auth"``, which is default. For + more, see the :ref:`RequestContext docs `. + +Users +~~~~~ + +When rendering a template :class:`~django.template.RequestContext`, the +currently logged-in user, either a :class:`~django.contrib.auth.models.User` +instance or an :class:`~django.contrib.auth.models.AnonymousUser` instance, is +stored in the template variable ``{{ user }}``: + +.. code-block:: html+django + + {% if user.is_authenticated %} +

    Welcome, {{ user.username }}. Thanks for logging in.

    + {% else %} +

    Welcome, new user. Please log in.

    + {% endif %} + +This template context variable is not available if a ``RequestContext`` is not +being used. + +Permissions +~~~~~~~~~~~ + +The currently logged-in user's permissions are stored in the template variable +``{{ perms }}``. This is an instance of +``django.contrib.auth.context_processors.PermWrapper``, which is a +template-friendly proxy of permissions. + +In the ``{{ perms }}`` object, single-attribute lookup is a proxy to +:meth:`User.has_module_perms `. +This example would display ``True`` if the logged-in user had any permissions +in the ``foo`` app:: + + {{ perms.foo }} + +Two-level-attribute lookup is a proxy to +:meth:`User.has_perm `. This example +would display ``True`` if the logged-in user had the permission +``foo.can_vote``:: + + {{ perms.foo.can_vote }} + +Thus, you can check permissions in template ``{% if %}`` statements: + +.. code-block:: html+django + + {% if perms.foo %} +

    You have permission to do something in the foo app.

    + {% if perms.foo.can_vote %} +

    You can vote!

    + {% endif %} + {% if perms.foo.can_drive %} +

    You can drive!

    + {% endif %} + {% else %} +

    You don't have permission to do anything in the foo app.

    + {% endif %} + +.. versionadded:: 1.5 + Permission lookup by "if in". + +It is possible to also look permissions up by ``{% if in %}`` statements. +For example: + +.. code-block:: html+django + + {% if 'foo' in perms %} + {% if 'foo.can_vote' in perms %} +

    In lookup works, too.

    + {% endif %} + {% endif %} + +.. _auth-admin: + +Managing users in the admin +=========================== + +When you have both ``django.contrib.admin`` and ``django.contrib.auth`` +installed, the admin provides a convenient way to view and manage users, +groups, and permissions. Users can be created and deleted like any Django +model. Groups can be created, and permissions can be assigned to users or +groups. A log of user edits to models made within the admin is also stored and +displayed. + +Creating Users +-------------- + +You should see a link to "Users" in the "Auth" +section of the main admin index page. The "Add user" admin page is different +than standard admin pages in that it requires you to choose a username and +password before allowing you to edit the rest of the user's fields. + +Also note: if you want a user account to be able to create users using the +Django admin site, you'll need to give them permission to add users *and* +change users (i.e., the "Add user" and "Change user" permissions). If an +account has permission to add users but not to change them, that account won't +be able to add users. Why? Because if you have permission to add users, you +have the power to create superusers, which can then, in turn, change other +users. So Django requires add *and* change permissions as a slight security +measure. + +Changing Passwords +------------------ + +User passwords are not displayed in the admin (nor stored in the database), but +the :doc:`password storage details ` are displayed. +Included in the display of this information is a link to +a password change form that allows admins to change user passwords. diff --git a/docs/topics/auth/index.txt b/docs/topics/auth/index.txt new file mode 100644 index 0000000000..ddb2d2f992 --- /dev/null +++ b/docs/topics/auth/index.txt @@ -0,0 +1,81 @@ +============================= +User authentication in Django +============================= + +.. toctree:: + :hidden: + + default + passwords + customizing + +.. module:: django.contrib.auth + :synopsis: Django's authentication framework. + +Django comes with an user authentication system. It handles user accounts, +groups, permissions and cookie-based user sessions. This section of the +documentation explains how the default implementation works out of the box, as +well as how to :doc:`extend and customize ` it to +suit your project's needs. + +Overview +======== + +The Django authentication system handles both authentication and authorization. +Briefly, authentication verifies a user is who they claim to be, and +authorization determines what an authenticated user is allowed to do. Here the +term authentication is used to refer to both tasks. + +The auth system consists of: + +* Users +* Permissions: Binary (yes/no) flags designating whether a user may perform + a certain task. +* Groups: A generic way of applying labels and permissions to more than one + user. +* A configurable password hashing system +* Forms and view tools for logging in users, or restricting content +* A pluggable backend system + +Installation +============ + +Authentication support is bundled as a Django contrib module in +``django.contrib.auth``. By default, the required configuration is already +included in the :file:`settings.py` generated by :djadmin:`django-admin.py +startproject `, these consist of two items listed in your +:setting:`INSTALLED_APPS` setting: + +1. ``'django.contrib.auth'`` contains the core of the authentication framework, + and its default models. +2. ``'django.contrib.contenttypes'`` is the Django :doc:`content type system + `, which allows permissions to be associated with + models you create. + +and two items in your :setting:`MIDDLEWARE_CLASSES` setting: + +1. :class:`~django.contrib.sessions.middleware.SessionMiddleware` manages + :doc:`sessions ` across requests. +2. :class:`~django.contrib.auth.middleware.AuthenticationMiddleware` associates + users with requests using sessions. + +With these settings in place, running the command ``manage.py syncdb`` creates +the necessary database tables for auth related models, creates permissions for +any models defined in your installed apps, and prompts you to create +a superuser account the first time you run it. + +Usage +===== + +:doc:`Using Django's default implementation ` + +* :ref:`Working with User objects ` +* :ref:`Permissions and authorization ` +* :ref:`Authentication in web requests ` +* :ref:`Managing users in the admin ` + +:doc:`API reference for the default implementation ` + +:doc:`Customizing Users and authentication ` + +:doc:`Password management in Django ` diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt new file mode 100644 index 0000000000..e6345aab2e --- /dev/null +++ b/docs/topics/auth/passwords.txt @@ -0,0 +1,212 @@ +============================= +Password management in Django +============================= + +Password management is something that should generally not be reinvented +unnecessarily, and Django endeavors to provide a secure and flexible set of +tools for managing user passwords. This document describes how Django stores +passwords, how the storage hashing can be configured, and some utilities to +work with hashed passwords. + +.. _auth_password_storage: + +How Django stores passwords +=========================== + +.. versionadded:: 1.4 + Django 1.4 introduces a new flexible password storage system and uses + PBKDF2 by default. Previous versions of Django used SHA1, and other + algorithms couldn't be chosen. + +The :attr:`~django.contrib.auth.models.User.password` attribute of a +:class:`~django.contrib.auth.models.User` object is a string in this format:: + + algorithm$hash + +That's a storage algorithm, and hash, separated by the dollar-sign +character. The algorithm is one of a number of one way hashing or password +storage algorithms Django can use; see below. The hash is the result of the one- +way function. + +By default, Django uses the PBKDF2_ algorithm with a SHA256 hash, a +password stretching mechanism recommended by NIST_. This should be +sufficient for most users: it's quite secure, requiring massive +amounts of computing time to break. + +However, depending on your requirements, you may choose a different +algorithm, or even use a custom algorithm to match your specific +security situation. Again, most users shouldn't need to do this -- if +you're not sure, you probably don't. If you do, please read on: + +Django chooses the an algorithm by consulting the :setting:`PASSWORD_HASHERS` +setting. This is a list of hashing algorithm classes that this Django +installation supports. The first entry in this list (that is, +``settings.PASSWORD_HASHERS[0]``) will be used to store passwords, and all the +other entries are valid hashers that can be used to check existing passwords. +This means that if you want to use a different algorithm, you'll need to modify +:setting:`PASSWORD_HASHERS` to list your preferred algorithm first in the list. + +The default for :setting:`PASSWORD_HASHERS` is:: + + PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.CryptPasswordHasher', + ) + +This means that Django will use PBKDF2_ to store all passwords, but will support +checking passwords stored with PBKDF2SHA1, bcrypt_, SHA1_, etc. The next few +sections describe a couple of common ways advanced users may want to modify this +setting. + +.. _bcrypt_usage: + +Using bcrypt with Django +------------------------ + +Bcrypt_ is a popular password storage algorithm that's specifically designed +for long-term password storage. It's not the default used by Django since it +requires the use of third-party libraries, but since many people may want to +use it Django supports bcrypt with minimal effort. + +To use Bcrypt as your default storage algorithm, do the following: + +1. Install the `py-bcrypt`_ library (probably by running ``sudo pip install + py-bcrypt``, or downloading the library and installing it with ``python + setup.py install``). + +2. Modify :setting:`PASSWORD_HASHERS` to list ``BCryptPasswordHasher`` + first. That is, in your settings file, you'd put:: + + PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.CryptPasswordHasher', + ) + + (You need to keep the other entries in this list, or else Django won't + be able to upgrade passwords; see below). + +That's it -- now your Django install will use Bcrypt as the default storage +algorithm. + +.. admonition:: Other bcrypt implementations + + There are several other implementations that allow bcrypt to be + used with Django. Django's bcrypt support is NOT directly + compatible with these. To upgrade, you will need to modify the + hashes in your database to be in the form `bcrypt$(raw bcrypt + output)`. For example: + `bcrypt$$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy`. + +Increasing the work factor +-------------------------- + +The PBKDF2 and bcrypt algorithms use a number of iterations or rounds of +hashing. This deliberately slows down attackers, making attacks against hashed +passwords harder. However, as computing power increases, the number of +iterations needs to be increased. We've chosen a reasonable default (and will +increase it with each release of Django), but you may wish to tune it up or +down, depending on your security needs and available processing power. To do so, +you'll subclass the appropriate algorithm and override the ``iterations`` +parameters. For example, to increase the number of iterations used by the +default PBKDF2 algorithm: + +1. Create a subclass of ``django.contrib.auth.hashers.PBKDF2PasswordHasher``:: + + from django.contrib.auth.hashers import PBKDF2PasswordHasher + + class MyPBKDF2PasswordHasher(PBKDF2PasswordHasher): + """ + A subclass of PBKDF2PasswordHasher that uses 100 times more iterations. + """ + iterations = PBKDF2PasswordHasher.iterations * 100 + + Save this somewhere in your project. For example, you might put this in + a file like ``myproject/hashers.py``. + +2. Add your new hasher as the first entry in :setting:`PASSWORD_HASHERS`:: + + PASSWORD_HASHERS = ( + 'myproject.hashers.MyPBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.CryptPasswordHasher', + ) + + +That's it -- now your Django install will use more iterations when it +stores passwords using PBKDF2. + +Password upgrading +------------------ + +When users log in, if their passwords are stored with anything other than +the preferred algorithm, Django will automatically upgrade the algorithm +to the preferred one. This means that old installs of Django will get +automatically more secure as users log in, and it also means that you +can switch to new (and better) storage algorithms as they get invented. + +However, Django can only upgrade passwords that use algorithms mentioned in +:setting:`PASSWORD_HASHERS`, so as you upgrade to new systems you should make +sure never to *remove* entries from this list. If you do, users using un- +mentioned algorithms won't be able to upgrade. + +.. _sha1: http://en.wikipedia.org/wiki/SHA1 +.. _pbkdf2: http://en.wikipedia.org/wiki/PBKDF2 +.. _nist: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf +.. _bcrypt: http://en.wikipedia.org/wiki/Bcrypt +.. _py-bcrypt: http://pypi.python.org/pypi/py-bcrypt/ + + +Manually managing a user's password +=================================== + +.. module:: django.contrib.auth.hashers + +.. versionadded:: 1.4 + The :mod:`django.contrib.auth.hashers` module provides a set of functions + to create and validate hashed password. You can use them independently + from the ``User`` model. + +.. function:: check_password(password, encoded) + + .. versionadded:: 1.4 + + If you'd like to manually authenticate a user by comparing a plain-text + password to the hashed password in the database, use the convenience + function :func:`django.contrib.auth.hashers.check_password`. It takes two + arguments: the plain-text password to check, and the full value of a + user's ``password`` field in the database to check against, and returns + ``True`` if they match, ``False`` otherwise. + +.. function:: make_password(password[, salt, hashers]) + + .. versionadded:: 1.4 + + Creates a hashed password in the format used by this application. It takes + one mandatory argument: the password in plain-text. Optionally, you can + provide a salt and a hashing algorithm to use, if you don't want to use the + defaults (first entry of ``PASSWORD_HASHERS`` setting). + Currently supported algorithms are: ``'pbkdf2_sha256'``, ``'pbkdf2_sha1'``, + ``'bcrypt'`` (see :ref:`bcrypt_usage`), ``'sha1'``, ``'md5'``, + ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` + if you have the ``crypt`` library installed. If the password argument is + ``None``, an unusable password is returned (a one that will be never + accepted by :func:`django.contrib.auth.hashers.check_password`). + +.. function:: is_password_usable(encoded_password) + + .. versionadded:: 1.4 + + Checks if the given string is a hashed password that has a chance + of being verified against :func:`django.contrib.auth.hashers.check_password`. diff --git a/docs/topics/db/models.txt b/docs/topics/db/models.txt index cfa794ca92..c4db0d77a7 100644 --- a/docs/topics/db/models.txt +++ b/docs/topics/db/models.txt @@ -1063,52 +1063,46 @@ Proxy models are declared like normal models. You tell Django that it's a proxy model by setting the :attr:`~django.db.models.Options.proxy` attribute of the ``Meta`` class to ``True``. -For example, suppose you want to add a method to the standard -:class:`~django.contrib.auth.models.User` model that will be used in your -templates. You can do it like this:: +For example, suppose you want to add a method to the ``Person`` model described +above. You can do it like this:: - from django.contrib.auth.models import User - - class MyUser(User): + class MyPerson(Person): class Meta: proxy = True def do_something(self): ... -The ``MyUser`` class operates on the same database table as its parent -:class:`~django.contrib.auth.models.User` class. In particular, any new -instances of :class:`~django.contrib.auth.models.User` will also be accessible -through ``MyUser``, and vice-versa:: +The ``MyPerson`` class operates on the same database table as its parent +``Person`` class. In particular, any new instances of ``Person`` will also be +accessible through ``MyPerson``, and vice-versa:: - >>> u = User.objects.create(username="foobar") - >>> MyUser.objects.get(username="foobar") - + >>> p = Person.objects.create(first_name="foobar") + >>> MyPerson.objects.get(first_name="foobar") + -You could also use a proxy model to define a different default ordering on a -model. The standard :class:`~django.contrib.auth.models.User` model has no -ordering defined on it (intentionally; sorting is expensive and we don't want -to do it all the time when we fetch users). You might want to regularly order -by the ``username`` attribute when you use the proxy. This is easy:: +You could also use a proxy model to define a different default ordering on +a model. You might not always want to order the ``Person`` model, but regularly +order by the ``last_name`` attribute when you use the proxy. This is easy:: - class OrderedUser(User): + class OrderedPerson(Person): class Meta: - ordering = ["username"] + ordering = ["last_name"] proxy = True -Now normal :class:`~django.contrib.auth.models.User` queries will be unordered -and ``OrderedUser`` queries will be ordered by ``username``. +Now normal ``Person`` queries will be unordered +and ``OrderedPerson`` queries will be ordered by ``last_name``. QuerySets still return the model that was requested ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -There is no way to have Django return, say, a ``MyUser`` object whenever you -query for :class:`~django.contrib.auth.models.User` objects. A queryset for -``User`` objects will return those types of objects. The whole point of proxy -objects is that code relying on the original ``User`` will use those and your -own code can use the extensions you included (that no other code is relying on -anyway). It is not a way to replace the ``User`` (or any other) model -everywhere with something of your own creation. +There is no way to have Django return, say, a ``MyPerson`` object whenever you +query for ``Person`` objects. A queryset for ``Person`` objects will return +those types of objects. The whole point of proxy objects is that code relying +on the original ``Person`` will use those and your own code can use the +extensions you included (that no other code is relying on anyway). It is not +a way to replace the ``Person`` (or any other) model everywhere with something +of your own creation. Base class restrictions ~~~~~~~~~~~~~~~~~~~~~~~ @@ -1131,12 +1125,12 @@ it will become the default, although any managers defined on the parent classes will still be available. Continuing our example from above, you could change the default manager used -when you query the ``User`` model like this:: +when you query the ``Person`` model like this:: class NewManager(models.Manager): ... - class MyUser(User): + class MyPerson(Person): objects = NewManager() class Meta: @@ -1154,7 +1148,7 @@ containing the new managers and inherit that after the primary base class:: class Meta: abstract = True - class MyUser(User, ExtraManagers): + class MyPerson(Person, ExtraManagers): class Meta: proxy = True diff --git a/docs/topics/index.txt b/docs/topics/index.txt index 82c5859b2c..a69318f05c 100644 --- a/docs/topics/index.txt +++ b/docs/topics/index.txt @@ -14,7 +14,7 @@ Introductions to all the key parts of Django you'll need to know: class-based-views/index files testing/index - auth + auth/index cache conditional-view-processing signing diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index c1c2a32f55..0627bd40d7 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -651,7 +651,7 @@ Use the ``django.test.client.Client`` class to make requests. .. method:: Client.login(**credentials) - If your site uses Django's :doc:`authentication system` + If your site uses Django's :doc:`authentication system` and you deal with logging in users, you can use the test client's ``login()`` method to simulate the effect of a user logging into the site. @@ -695,7 +695,7 @@ Use the ``django.test.client.Client`` class to make requests. .. method:: Client.logout() - If your site uses Django's :doc:`authentication system`, + If your site uses Django's :doc:`authentication system`, the ``logout()`` method can be used to simulate the effect of a user logging out of your site. -- cgit v1.3 From 067505ad19f088e8db1d8c788ceea388c7241bcd Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 29 Dec 2012 10:35:12 -0500 Subject: Fixed broken links, round 4. refs #19516 --- docs/howto/auth-remote-user.txt | 2 ++ docs/howto/custom-management-commands.txt | 9 ++++++ docs/howto/custom-model-fields.txt | 20 ++++++------ docs/internals/deprecation.txt | 30 ++++++++--------- docs/ref/contrib/auth.txt | 8 ++--- docs/ref/contrib/gis/testing.txt | 2 ++ docs/ref/contrib/messages.txt | 2 ++ docs/ref/signals.txt | 17 ++++------ docs/releases/1.0-porting-guide.txt | 13 ++++---- docs/releases/1.1-beta-1.txt | 2 +- docs/releases/1.1.txt | 19 ++++++----- docs/releases/1.2.4.txt | 2 +- docs/releases/1.3-alpha-1.txt | 36 ++++++++++----------- docs/releases/1.3-beta-1.txt | 9 +++--- docs/releases/1.3.txt | 53 ++++++++++++++----------------- docs/releases/1.4-alpha-1.txt | 2 +- docs/releases/1.4-beta-1.txt | 2 +- docs/releases/1.4.txt | 2 +- docs/releases/1.5-alpha-1.txt | 9 +++--- docs/releases/1.5-beta-1.txt | 8 ++--- docs/releases/1.5.txt | 10 +++--- docs/topics/class-based-views/mixins.txt | 39 +++++++++++------------ docs/topics/db/queries.txt | 4 +++ docs/topics/forms/formsets.txt | 2 +- docs/topics/forms/index.txt | 6 ++-- docs/topics/forms/modelforms.txt | 2 ++ docs/topics/http/sessions.txt | 2 +- docs/topics/i18n/translation.txt | 2 ++ docs/topics/pagination.txt | 4 +-- docs/topics/testing/overview.txt | 4 +-- 30 files changed, 164 insertions(+), 158 deletions(-) (limited to 'docs/internals') diff --git a/docs/howto/auth-remote-user.txt b/docs/howto/auth-remote-user.txt index deab794cb1..d59bb25a85 100644 --- a/docs/howto/auth-remote-user.txt +++ b/docs/howto/auth-remote-user.txt @@ -27,6 +27,8 @@ use of the ``REMOTE_USER`` value using the ``RemoteUserMiddleware`` and Configuration ============= +.. class:: django.contrib.auth.middleware.RemoteUserMiddleware + First, you must add the :class:`django.contrib.auth.middleware.RemoteUserMiddleware` to the :setting:`MIDDLEWARE_CLASSES` setting **after** the diff --git a/docs/howto/custom-management-commands.txt b/docs/howto/custom-management-commands.txt index 12e8ec2494..6a7f644218 100644 --- a/docs/howto/custom-management-commands.txt +++ b/docs/howto/custom-management-commands.txt @@ -2,6 +2,8 @@ Writing custom django-admin commands ==================================== +.. module:: django.core.management + Applications can register their own actions with ``manage.py``. For example, you might want to add a ``manage.py`` action for a Django app that you're distributing. In this document, we will be building a custom ``closepoll`` @@ -261,6 +263,13 @@ the :meth:`~BaseCommand.handle` method must be implemented. The actual logic of the command. Subclasses must implement this method. +.. method:: BaseCommand.validate(app=None, display_num_errors=False) + + Validates the given app, raising :class:`CommandError` for any errors. + + If ``app`` is None, then all installed apps are validated. + + .. _ref-basecommand-subclasses: BaseCommand subclasses diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 1e9d5d8701..dd57da5d45 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -153,8 +153,8 @@ class, from which everything is descended. Initializing your new field is a matter of separating out any arguments that are specific to your case from the common arguments and passing the latter to the -:meth:`~django.db.models.Field.__init__` method of -:class:`~django.db.models.Field` (or your parent class). +``__init__()`` method of :class:`~django.db.models.Field` (or your parent +class). In our example, we'll call our field ``HandField``. (It's a good idea to call your :class:`~django.db.models.Field` subclass ``Field``, so it's @@ -602,11 +602,11 @@ Returns the default form field to use when this field is displayed in a model. This method is called by the :class:`~django.forms.ModelForm` helper. All of the ``kwargs`` dictionary is passed directly to the form field's -:meth:`~django.forms.Field__init__` method. Normally, all you need to do is -set up a good default for the ``form_class`` argument and then delegate further -handling to the parent class. This might require you to write a custom form -field (and even a form widget). See the :doc:`forms documentation -` for information about this, and take a look at the code in +``__init__()`` method. Normally, all you need to do is set up a good default +for the ``form_class`` argument and then delegate further handling to the +parent class. This might require you to write a custom form field (and even a +form widget). See the :doc:`forms documentation ` for +information about this, and take a look at the code in :mod:`django.contrib.localflavor` for some examples of custom widgets. Continuing our ongoing example, we can write the :meth:`.formfield` method as:: @@ -668,7 +668,7 @@ Converting field data for serialization .. method:: Field.value_to_string(self, obj) This method is used by the serializers to convert the field into a string for -output. Calling :meth:`Field._get_val_from_obj(obj)` is the best way to get the +output. Calling ``Field._get_val_from_obj(obj)`` is the best way to get the value to serialize. For example, since our ``HandField`` uses strings for its data storage anyway, we can reuse some existing conversion code:: @@ -692,12 +692,12 @@ smoothly: a field that's similar to what you want and extend it a little bit, instead of creating an entirely new field from scratch. -2. Put a :meth:`__str__` or :meth:`__unicode__` method on the class you're +2. Put a ``__str__()`` or ``__unicode__()`` method on the class you're wrapping up as a field. There are a lot of places where the default behavior of the field code is to call :func:`~django.utils.encoding.force_text` on the value. (In our examples in this document, ``value`` would be a ``Hand`` instance, not a - ``HandField``). So if your :meth:`__unicode__` method automatically + ``HandField``). So if your ``__unicode__()`` method automatically converts to the string form of your Python object, you can save yourself a lot of work. diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 77f03ae2c7..74f544c220 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -20,7 +20,7 @@ these changes. * The old imports for CSRF functionality (``django.contrib.csrf.*``), which moved to core in 1.2, will be removed. -* The :mod:`django.contrib.gis.db.backend` module will be removed in favor +* The ``django.contrib.gis.db.backend`` module will be removed in favor of the specific backends. * ``SMTPConnection`` will be removed in favor of a generic Email backend API. @@ -122,23 +122,23 @@ these changes. The :attr:`~django.test.client.Response.templates` attribute should be used instead. -* The :class:`~django.test.simple.DjangoTestRunner` will be removed. +* The ``django.test.simple.DjangoTestRunner`` will be removed. Instead use a unittest-native class. The features of the - :class:`django.test.simple.DjangoTestRunner` (including fail-fast and + ``django.test.simple.DjangoTestRunner`` (including fail-fast and Ctrl-C test termination) can currently be provided by the unittest-native - :class:`TextTestRunner`. + :class:`~unittest.TextTestRunner`. * The undocumented function - :func:`django.contrib.formtools.utils.security_hash` will be removed, - instead use :func:`django.contrib.formtools.utils.form_hmac` + ``django.contrib.formtools.utils.security_hash`` will be removed, + instead use ``django.contrib.formtools.utils.form_hmac`` * The function-based generic view modules will be removed in favor of their class-based equivalents, outlined :doc:`here `. -* The :class:`~django.core.servers.basehttp.AdminMediaHandler` will be +* The ``django.core.servers.basehttp.AdminMediaHandler`` will be removed. In its place use - :class:`~django.contrib.staticfiles.handlers.StaticFilesHandler`. + ``django.contrib.staticfiles.handlers.StaticFilesHandler``. * The template tags library ``adminmedia`` and the template tag ``{% admin_media_prefix %}`` will be removed in favor of the generic static files @@ -150,8 +150,7 @@ these changes. an implied string. In 1.4, this behavior is provided by a version of the tag in the ``future`` template tag library. -* The :djadmin:`reset` and :djadmin:`sqlreset` management commands - will be removed. +* The ``reset`` and ``sqlreset`` management commands will be removed. * Authentication backends will need to support an inactive user being passed to all methods dealing with permissions. @@ -162,11 +161,11 @@ these changes. a :class:`~django.contrib.gis.geos.GEOSException` when called on a geometry with no SRID value. -* :class:`~django.http.CompatCookie` will be removed in favor of - :class:`~django.http.SimpleCookie`. +* ``django.http.CompatCookie`` will be removed in favor of + ``django.http.SimpleCookie``. -* :class:`django.core.context_processors.PermWrapper` and - :class:`django.core.context_processors.PermLookupDict` will be removed in +* ``django.core.context_processors.PermWrapper`` and + ``django.core.context_processors.PermLookupDict`` will be removed in favor of the corresponding :class:`django.contrib.auth.context_processors.PermWrapper` and :class:`django.contrib.auth.context_processors.PermLookupDict`, @@ -213,8 +212,7 @@ these changes. ``django.utils.itercompat.all`` and ``django.utils.itercompat.any`` will be removed. The Python builtin versions should be used instead. -* The :func:`~django.views.decorators.csrf.csrf_response_exempt` and - :func:`~django.views.decorators.csrf.csrf_view_exempt` decorators will +* The ``csrf_response_exempt`` and ``csrf_view_exempt`` decorators will be removed. Since 1.4 ``csrf_response_exempt`` has been a no-op (it returns the same function), and ``csrf_view_exempt`` has been a synonym for ``django.views.decorators.csrf.csrf_exempt``, which should diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 41f218b0a4..74a69c1f7d 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -349,7 +349,7 @@ Login and logout signals The auth framework uses two :doc:`signals ` that can be used for notification when a user logs in or out. -.. function:: django.contrib.auth.signals.user_logged_in +.. function:: user_logged_in Sent when a user logs in successfully. @@ -364,7 +364,7 @@ for notification when a user logs in or out. ``user`` The user instance that just logged in. -.. function:: django.contrib.auth.signals.user_logged_out +.. function:: user_logged_out Sent when the logout method is called. @@ -379,9 +379,9 @@ for notification when a user logs in or out. The user instance that just logged out or ``None`` if the user was not authenticated. -.. function:: django.contrib.auth.signals.user_login_failed +.. function:: user_login_failed -.. versionadded:: 1.5 + .. versionadded:: 1.5 Sent when the user failed to login successfully diff --git a/docs/ref/contrib/gis/testing.txt b/docs/ref/contrib/gis/testing.txt index 86979f0308..2a6dcef46f 100644 --- a/docs/ref/contrib/gis/testing.txt +++ b/docs/ref/contrib/gis/testing.txt @@ -140,6 +140,8 @@ with the rest of :ref:`Django's unit tests `. Run only GeoDjango tests ------------------------ +.. class:: django.contrib.gis.tests.GeoDjangoTestSuiteRunner + To run *only* the tests for GeoDjango, the :setting:`TEST_RUNNER` setting must be changed to use the :class:`~django.contrib.gis.tests.GeoDjangoTestSuiteRunner`:: diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt index 4fa733edb5..661d7f2103 100644 --- a/docs/ref/contrib/messages.txt +++ b/docs/ref/contrib/messages.txt @@ -149,6 +149,8 @@ tags for the levels you wish to override:: Using messages in views and templates ===================================== +.. function:: add_message(request, level, message, extra_tags='', fail_silently=False) + Adding a message ---------------- diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index 0671d80b7c..c31c90f4e8 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -27,9 +27,8 @@ module system. .. warning:: Many of these signals are sent by various model methods like - :meth:`~django.db.models.Model.__init__` or - :meth:`~django.db.models.Model.save` that you can overwrite in your own - code. + ``__init__()`` or :meth:`~django.db.models.Model.save` that you can + override in your own code. If you override these methods on your model, you must call the parent class' methods for this signals to be sent. @@ -47,7 +46,7 @@ pre_init .. ^^^^^^^ this :module: hack keeps Sphinx from prepending the module. Whenever you instantiate a Django model, this signal is sent at the beginning -of the model's :meth:`~django.db.models.Model.__init__` method. +of the model's ``__init__()`` method. Arguments sent with this signal: @@ -55,12 +54,10 @@ Arguments sent with this signal: The model class that just had an instance created. ``args`` - A list of positional arguments passed to - :meth:`~django.db.models.Model.__init__`: + A list of positional arguments passed to ``__init__()``: ``kwargs`` - A dictionary of keyword arguments passed to - :meth:`~django.db.models.Model.__init__`:. + A dictionary of keyword arguments passed to ``__init__()``: For example, the :doc:`tutorial ` has this line:: @@ -74,7 +71,7 @@ Argument Value ``sender`` ``Poll`` (the class itself) ``args`` ``[]`` (an empty list because there were no positional - arguments passed to ``__init__``.) + arguments passed to ``__init__()``.) ``kwargs`` ``{'question': "What's up?", 'pub_date': datetime.now()}`` ========== =============================================================== @@ -85,7 +82,7 @@ post_init .. data:: django.db.models.signals.post_init :module: -Like pre_init, but this one is sent when the :meth:`~django.db.models.Model.__init__`: method finishes. +Like pre_init, but this one is sent when the ``__init__()`` method finishes. Arguments sent with this signal: diff --git a/docs/releases/1.0-porting-guide.txt b/docs/releases/1.0-porting-guide.txt index ae73baa072..644350525c 100644 --- a/docs/releases/1.0-porting-guide.txt +++ b/docs/releases/1.0-porting-guide.txt @@ -277,8 +277,9 @@ Handle uploaded files using the new API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Replace use of uploaded files -- that is, entries in ``request.FILES`` -- as -simple dictionaries with the new :class:`~django.core.files.UploadedFile`. The -old dictionary syntax no longer works. +simple dictionaries with the new +:class:`~django.core.files.uploadedfile.UploadedFile`. The old dictionary +syntax no longer works. Thus, in a view like:: @@ -410,7 +411,7 @@ U.S. local flavor ~~~~~~~~~~~~~~~~~ ``django.contrib.localflavor.usa`` has been renamed to -:mod:`django.contrib.localflavor.us`. This change was made to match the naming +``django.contrib.localflavor.us``. This change was made to match the naming scheme of other local flavors. To migrate your code, all you need to do is change the imports. @@ -642,8 +643,8 @@ The generic relation classes -- ``GenericForeignKey`` and ``GenericRelation`` Testing ------- -:meth:`django.test.Client.login` has changed -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:meth:`django.test.client.Client.login` has changed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Old (0.96):: @@ -721,7 +722,7 @@ To update your code: 1. Use :class:`django.utils.datastructures.SortedDict` wherever you were using ``django.newforms.forms.SortedDictFromList``. -2. Because :meth:`django.utils.datastructures.SortedDict.copy` doesn't +2. Because ``django.utils.datastructures.SortedDict.copy`` doesn't return a deepcopy as ``SortedDictFromList.copy()`` did, you will need to update your code if you were relying on a deepcopy. Do this by using ``copy.deepcopy`` directly. diff --git a/docs/releases/1.1-beta-1.txt b/docs/releases/1.1-beta-1.txt index 1555a9464a..88d8ce5f35 100644 --- a/docs/releases/1.1-beta-1.txt +++ b/docs/releases/1.1-beta-1.txt @@ -36,7 +36,7 @@ A number of features have been added to Django's model layer: You can now control whether or not Django creates database tables for a model using the :attr:`~Options.managed` model option. This defaults to ``True``, meaning that Django will create the appropriate database tables in -:djadmin:`syncdb` and remove them as part of :djadmin:`reset` command. That +:djadmin:`syncdb` and remove them as part of ``reset`` command. That is, Django *manages* the database table's lifecycle. If you set this to ``False``, however, no database table creating or deletion diff --git a/docs/releases/1.1.txt b/docs/releases/1.1.txt index 68fc624924..ca8e1fff2a 100644 --- a/docs/releases/1.1.txt +++ b/docs/releases/1.1.txt @@ -37,7 +37,7 @@ If you are using a 32-bit platform, you're off the hook; you'll observe no differences as a result of this change. However, **users on 64-bit platforms may experience some problems** using the -:djadmin:`reset` management command. Prior to this change, 64-bit platforms +``reset`` management command. Prior to this change, 64-bit platforms would generate a 64-bit, 16 character digest in the constraint name; for example:: @@ -48,14 +48,14 @@ Following this change, all platforms, regardless of word size, will generate a ALTER TABLE myapp_sometable ADD CONSTRAINT object_id_refs_id_32091d1e FOREIGN KEY ... -As a result of this change, you will not be able to use the :djadmin:`reset` +As a result of this change, you will not be able to use the ``reset`` management command on any table made by a 64-bit machine. This is because the the new generated name will not match the historically generated name; as a result, the SQL constructed by the reset command will be invalid. If you need to reset an application that was created with 64-bit constraints, you will need to manually drop the old constraint prior to invoking -:djadmin:`reset`. +``reset``. Test cases are now run in a transaction --------------------------------------- @@ -120,9 +120,8 @@ has been saved. Changes to how model formsets are saved --------------------------------------- -.. currentmodule:: django.forms.models - -In Django 1.1, :class:`BaseModelFormSet` now calls :meth:`ModelForm.save()`. +In Django 1.1, :class:`~django.forms.models.BaseModelFormSet` now calls +``ModelForm.save()``. This is backwards-incompatible if you were modifying ``self.initial`` in a model formset's ``__init__``, or if you relied on the internal ``_total_form_count`` @@ -146,7 +145,7 @@ Permanent redirects and the ``redirect_to()`` generic view ---------------------------------------------------------- Django 1.1 adds a ``permanent`` argument to the -:func:`django.views.generic.simple.redirect_to()` view. This is technically +``django.views.generic.simple.redirect_to()`` view. This is technically backwards-incompatible if you were using the ``redirect_to`` view with a format-string key called 'permanent', which is highly unlikely. @@ -211,8 +210,8 @@ Query expressions Queries can now refer to a another field on the query and can traverse relationships to refer to fields on related models. This is implemented in the -new :class:`F` object; for full details, including examples, consult the -:ref:`documentation for F expressions `. +new :class:`~django.db.models.F` object; for full details, including examples, +consult the :ref:`documentation for F expressions `. Model improvements ------------------ @@ -225,7 +224,7 @@ A number of features have been added to Django's model layer: You can now control whether or not Django manages the life-cycle of the database tables for a model using the :attr:`~Options.managed` model option. This defaults to ``True``, meaning that Django will create the appropriate database -tables in :djadmin:`syncdb` and remove them as part of the :djadmin:`reset` +tables in :djadmin:`syncdb` and remove them as part of the ``reset`` command. That is, Django *manages* the database table's lifecycle. If you set this to ``False``, however, no database table creating or deletion diff --git a/docs/releases/1.2.4.txt b/docs/releases/1.2.4.txt index cd4ab76f55..b74ea9aef2 100644 --- a/docs/releases/1.2.4.txt +++ b/docs/releases/1.2.4.txt @@ -76,7 +76,7 @@ GeoDjango ========= The function-based :setting:`TEST_RUNNER` previously used to execute -the GeoDjango test suite, :func:`django.contrib.gis.tests.run_gis_tests`, +the GeoDjango test suite, ``django.contrib.gis.tests.run_gis_tests``, was finally deprecated in favor of a class-based test runner, :class:`django.contrib.gis.tests.GeoDjangoTestSuiteRunner`, added in this release. diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index bb7f2dbb73..e2c52a7264 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -311,37 +311,35 @@ As a result of the introduction of class-based generic views, the function-based generic views provided by Django have been deprecated. The following modules and the views they contain have been deprecated: -* :mod:`django.views.generic.create_update` -* :mod:`django.views.generic.date_based` -* :mod:`django.views.generic.list_detail` -* :mod:`django.views.generic.simple` +* ``django.views.generic.create_update`` +* ``django.views.generic.date_based`` +* ``django.views.generic.list_detail`` +* ``django.views.generic.simple`` Test client response ``template`` attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django's :ref:`test client ` returns :class:`~django.test.client.Response` objects annotated with extra testing -information. In Django versions prior to 1.3, this included a -:attr:`~django.test.client.Response.template` attribute containing information -about templates rendered in generating the response: either None, a single -:class:`~django.template.Template` object, or a list of -:class:`~django.template.Template` objects. This inconsistency in return values -(sometimes a list, sometimes not) made the attribute difficult to work with. - -In Django 1.3 the :attr:`~django.test.client.Response.template` attribute is -deprecated in favor of a new :attr:`~django.test.client.Response.templates` -attribute, which is always a list, even if it has only a single element or no -elements. +information. In Django versions prior to 1.3, this included a ``template`` +attribute containing information about templates rendered in generating the +response: either None, a single :class:`~django.template.Template` object, or a +list of :class:`~django.template.Template` objects. This inconsistency in +return values (sometimes a list, sometimes not) made the attribute difficult +to work with. + +In Django 1.3 the ``template`` attribute is deprecated in favor of a new +:attr:`~django.test.client.Response.templates` attribute, which is always a +list, even if it has only a single element or no elements. ``DjangoTestRunner`` ~~~~~~~~~~~~~~~~~~~~ As a result of the introduction of support for unittest2, the features -of :class:`django.test.simple.DjangoTestRunner` (including fail-fast +of ``django.test.simple.DjangoTestRunner`` (including fail-fast and Ctrl-C test termination) have been made redundant. In view of this -redundancy, :class:`~django.test.simple.DjangoTestRunner` has been -turned into an empty placeholder class, and will be removed entirely -in Django 1.5. +redundancy, ``DjangoTestRunner`` has been turned into an empty placeholder +class, and will be removed entirely in Django 1.5. The Django 1.3 roadmap ====================== diff --git a/docs/releases/1.3-beta-1.txt b/docs/releases/1.3-beta-1.txt index 2729c7f2ba..d064063fce 100644 --- a/docs/releases/1.3-beta-1.txt +++ b/docs/releases/1.3-beta-1.txt @@ -142,10 +142,9 @@ Changes to ``USStateField`` The :mod:`django.contrib.localflavor` application contains collections of code relevant to specific countries or cultures. One such is -:class:`~django.contrib.localflavor.us.models.USStateField`, which -provides a field for storing the two-letter postal abbreviation of a -U.S. state. This field has consistently caused problems, however, -because it is often used to store the state portion of a U.S postal +``USStateField``, which provides a field for storing the two-letter postal +abbreviation of a U.S. state. This field has consistently caused problems, +however, because it is often used to store the state portion of a U.S postal address, but not all "states" recognized by the U.S Postal Service are actually states of the U.S. or even U.S. territory. Several compromises over the list of choices resulted in some users feeling @@ -161,7 +160,7 @@ as a pair of changes: choices, plus the U.S. Armed Forces postal codes. * A new model field, - :class:`django.contrib.localflavor.us.models.USPostalCodeField`, has + ``django.contrib.localflavor.us.models.USPostalCodeField``, has been added which draws its choices from a list of all postal abbreviations recognized by the U.S Postal Service. This includes all abbreviations recognized by `USStateField`, plus three diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index d6ef11d113..6a056532b9 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -700,40 +700,35 @@ As a result of the introduction of class-based generic views, the function-based generic views provided by Django have been deprecated. The following modules and the views they contain have been deprecated: -* :mod:`django.views.generic.create_update` - -* :mod:`django.views.generic.date_based` - -* :mod:`django.views.generic.list_detail` - -* :mod:`django.views.generic.simple` +* ``django.views.generic.create_update`` +* ``django.views.generic.date_based`` +* ``django.views.generic.list_detail`` +* ``django.views.generic.simple`` Test client response ``template`` attribute ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Django's :ref:`test client ` returns :class:`~django.test.client.Response` objects annotated with extra testing -information. In Django versions prior to 1.3, this included a -:attr:`~django.test.client.Response.template` attribute containing information -about templates rendered in generating the response: either None, a single -:class:`~django.template.Template` object, or a list of -:class:`~django.template.Template` objects. This inconsistency in return values -(sometimes a list, sometimes not) made the attribute difficult to work with. - -In Django 1.3 the :attr:`~django.test.client.Response.template` attribute is -deprecated in favor of a new :attr:`~django.test.client.Response.templates` -attribute, which is always a list, even if it has only a single element or no -elements. +information. In Django versions prior to 1.3, this included a ``template`` +attribute containing information about templates rendered in generating the +response: either None, a single :class:`~django.template.Template` object, or a +list of :class:`~django.template.Template` objects. This inconsistency in +return values (sometimes a list, sometimes not) made the attribute difficult +to work with. + +In Django 1.3 the ``template`` attribute is deprecated in favor of a new +:attr:`~django.test.client.Response.templates` attribute, which is always a +list, even if it has only a single element or no elements. ``DjangoTestRunner`` ~~~~~~~~~~~~~~~~~~~~ As a result of the introduction of support for unittest2, the features -of :class:`django.test.simple.DjangoTestRunner` (including fail-fast +of ``django.test.simple.DjangoTestRunner`` (including fail-fast and Ctrl-C test termination) have been made redundant. In view of this -redundancy, :class:`~django.test.simple.DjangoTestRunner` has been -turned into an empty placeholder class, and will be removed entirely -in Django 1.5. +redundancy, ``DjangoTestRunner`` has been turned into an empty placeholder +class, and will be removed entirely in Django 1.5. Changes to :ttag:`url` and :ttag:`ssi` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -805,9 +800,8 @@ GeoDjango ~~~~~~~~~ * The function-based :setting:`TEST_RUNNER` previously used to execute - the GeoDjango test suite, - :func:`django.contrib.gis.tests.run_gis_tests`, was deprecated for - the class-based runner, + the GeoDjango test suite, ``django.contrib.gis.tests.run_gis_tests``, was + deprecated for the class-based runner, :class:`django.contrib.gis.tests.GeoDjangoTestSuiteRunner`. * Previously, calling @@ -886,11 +880,10 @@ identical to their old versions; only the module location has changed. Removal of ``XMLField`` ~~~~~~~~~~~~~~~~~~~~~~~ -When Django was first released, Django included an -:class:`~django.db.models.XMLField` that performed automatic XML validation -for any field input. However, this validation function hasn't been -performed since the introduction of ``newforms``, prior to the 1.0 release. -As a result, ``XMLField`` as currently implemented is functionally +When Django was first released, Django included an ``XMLField`` that performed +automatic XML validation for any field input. However, this validation function +hasn't been performed since the introduction of ``newforms``, prior to the 1.0 +release. As a result, ``XMLField`` as currently implemented is functionally indistinguishable from a simple :class:`~django.db.models.TextField`. For this reason, Django 1.3 has fast-tracked the deprecation of diff --git a/docs/releases/1.4-alpha-1.txt b/docs/releases/1.4-alpha-1.txt index 3c6f7e9b27..fc19e90384 100644 --- a/docs/releases/1.4-alpha-1.txt +++ b/docs/releases/1.4-alpha-1.txt @@ -503,7 +503,7 @@ Django 1.4 also includes several smaller improvements worth noting: * In the documentation, a helpful :doc:`security overview ` page. -* The :func:`django.contrib.auth.models.check_password` function has been moved +* The ``django.contrib.auth.models.check_password`` function has been moved to the :mod:`django.contrib.auth.utils` module. Importing it from the old location will still work, but you should update your imports. diff --git a/docs/releases/1.4-beta-1.txt b/docs/releases/1.4-beta-1.txt index 2a1041bcd0..2c84d21b8d 100644 --- a/docs/releases/1.4-beta-1.txt +++ b/docs/releases/1.4-beta-1.txt @@ -563,7 +563,7 @@ Django 1.4 also includes several smaller improvements worth noting: * In the documentation, a helpful :doc:`security overview ` page. -* The :func:`django.contrib.auth.models.check_password` function has been moved +* The ``django.contrib.auth.models.check_password`` function has been moved to the :mod:`django.contrib.auth.utils` module. Importing it from the old location will still work, but you should update your imports. diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 746ed58945..d700ba8b89 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -585,7 +585,7 @@ Django 1.4 also includes several smaller improvements worth noting: * In the documentation, a helpful :doc:`security overview ` page. -* The :func:`django.contrib.auth.models.check_password` function has been moved +* The ``django.contrib.auth.models.check_password`` function has been moved to the :mod:`django.contrib.auth.hashers` module. Importing it from the old location will still work, but you should update your imports. diff --git a/docs/releases/1.5-alpha-1.txt b/docs/releases/1.5-alpha-1.txt index b167bb1879..c2ad691a76 100644 --- a/docs/releases/1.5-alpha-1.txt +++ b/docs/releases/1.5-alpha-1.txt @@ -423,7 +423,7 @@ More information on these incompatibilities is available in `ticket #18023`_. The net result is that, if you have installed :mod:`simplejson` and your code uses Django's serialization internals directly -- for instance -:class:`django.core.serializers.json.DjangoJSONEncoder`, the switch from +``django.core.serializers.json.DjangoJSONEncoder``, the switch from :mod:`simplejson` to :mod:`json` could break your code. (In general, changes to internals aren't documented; we're making an exception here.) @@ -449,8 +449,8 @@ When using :doc:`object pagination `, the ``previous_page_number()`` and ``next_page_number()`` methods of the :class:`~django.core.paginator.Page` object did not check if the returned number was inside the existing page range. -It does check it now and raises an :exc:`InvalidPage` exception when the number -is either too low or too high. +It does check it now and raises an :exc:`~django.core.paginator.InvalidPage` +exception when the number is either too low or too high. Behavior of autocommit database option on PostgreSQL changed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -619,10 +619,9 @@ Define a ``__str__`` method and apply the ``django.utils.itercompat.product`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :func:`~django.utils.itercompat.product` function has been deprecated. Use +The ``django.utils.itercompat.product`` function has been deprecated. Use the built-in :func:`itertools.product` instead. - ``django.utils.markup`` ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.5-beta-1.txt b/docs/releases/1.5-beta-1.txt index 7208d9657c..4dbe77f806 100644 --- a/docs/releases/1.5-beta-1.txt +++ b/docs/releases/1.5-beta-1.txt @@ -448,7 +448,7 @@ More information on these incompatibilities is available in `ticket #18023`_. The net result is that, if you have installed :mod:`simplejson` and your code uses Django's serialization internals directly -- for instance -:class:`django.core.serializers.json.DjangoJSONEncoder`, the switch from +``django.core.serializers.json.DjangoJSONEncoder``, the switch from :mod:`simplejson` to :mod:`json` could break your code. (In general, changes to internals aren't documented; we're making an exception here.) @@ -474,8 +474,8 @@ When using :doc:`object pagination `, the ``previous_page_number()`` and ``next_page_number()`` methods of the :class:`~django.core.paginator.Page` object did not check if the returned number was inside the existing page range. -It does check it now and raises an :exc:`InvalidPage` exception when the number -is either too low or too high. +It does check it now and raises an :exc:`~django.core.paginator.InvalidPage` +exception when the number is either too low or too high. Behavior of autocommit database option on PostgreSQL changed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -672,7 +672,7 @@ Define a ``__str__`` method and apply the ``django.utils.itercompat.product`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :func:`~django.utils.itercompat.product` function has been deprecated. Use +The ``django.utils.itercompat.product`` function has been deprecated. Use the built-in :func:`itertools.product` instead. ``django.utils.markup`` diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index b0f0bee293..a449f4ab12 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -100,7 +100,7 @@ Some features of Django aren't available because they depend on third-party software that hasn't been ported to Python 3 yet, including: - the MySQL database backend (depends on MySQLdb) -- :class:`~django.db.models.fields.ImageField` (depends on PIL) +- :class:`~django.db.models.ImageField` (depends on PIL) - :class:`~django.test.LiveServerTestCase` (depends on Selenium WebDriver) Further, Django's more than a web framework; it's an ecosystem of pluggable @@ -469,7 +469,7 @@ More information on these incompatibilities is available in `ticket #18023`_. The net result is that, if you have installed :mod:`simplejson` and your code uses Django's serialization internals directly -- for instance -:class:`django.core.serializers.json.DjangoJSONEncoder`, the switch from +``django.core.serializers.json.DjangoJSONEncoder``, the switch from :mod:`simplejson` to :mod:`json` could break your code. (In general, changes to internals aren't documented; we're making an exception here.) @@ -495,8 +495,8 @@ When using :doc:`object pagination `, the ``previous_page_number()`` and ``next_page_number()`` methods of the :class:`~django.core.paginator.Page` object did not check if the returned number was inside the existing page range. -It does check it now and raises an :exc:`InvalidPage` exception when the number -is either too low or too high. +It does check it now and raises an :exc:`~django.core.paginator.InvalidPage` +exception when the number is either too low or too high. Behavior of autocommit database option on PostgreSQL changed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -714,7 +714,7 @@ Define a ``__str__`` method and apply the ``django.utils.itercompat.product`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :func:`~django.utils.itercompat.product` function has been deprecated. Use +The ``django.utils.itercompat.product`` function has been deprecated. Use the built-in :func:`itertools.product` instead. ``cleanup`` management command diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index f349c23626..923b877cc5 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -93,8 +93,8 @@ DetailView: working with a single Django object To show the detail of an object, we basically need to do two things: we need to look up the object and then we need to make a -:class:`TemplateResponse` with a suitable template, and that object as -context. +:class:`~django.template.response.TemplateResponse` with a suitable template, +and that object as context. To get the object, :class:`~django.views.generic.detail.DetailView` relies on :class:`~django.views.generic.detail.SingleObjectMixin`, @@ -111,15 +111,14 @@ attribute if that's provided). :class:`SingleObjectMixin` also overrides which is used across all Django's built in class-based views to supply context data for template renders. -To then make a :class:`TemplateResponse`, :class:`DetailView` uses +To then make a :class:`~django.template.response.TemplateResponse`, +:class:`DetailView` uses :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, -which extends -:class:`~django.views.generic.base.TemplateResponseMixin`, overriding -:meth:`get_template_names()` as discussed above. It actually provides -a fairly sophisticated set of options, but the main one that most -people are going to use is -``/_detail.html``. The ``_detail`` part can be -changed by setting +which extends :class:`~django.views.generic.base.TemplateResponseMixin`, +overriding :meth:`get_template_names()` as discussed above. It actually +provides a fairly sophisticated set of options, but the main one that most +people are going to use is ``/_detail.html``. The +``_detail`` part can be changed by setting :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` on a subclass to something else. (For instance, the :doc:`generic edit views` use ``_form`` for create and update views, and @@ -265,7 +264,7 @@ We can hook this into our URLs easily enough:: Note the ``pk`` named group, which :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` uses -to look up the :class:`Author` instance. You could also use a slug, or +to look up the ``Author`` instance. You could also use a slug, or any of the other features of :class:`SingleObjectMixin`. Using SingleObjectMixin with ListView @@ -299,7 +298,7 @@ object. In order to do this, we need to have two different querysets: will add in the suitable ``page_obj`` and ``paginator`` for us providing we remember to call ``super()``. -Now we can write a new :class:`PublisherDetail`:: +Now we can write a new ``PublisherDetail``:: from django.views.generic import ListView from django.views.generic.detail import SingleObjectMixin @@ -403,7 +402,7 @@ At this point it's natural to reach for a :class:`Form` to encapsulate the information sent from the user's browser to Django. Say also that we're heavily invested in `REST`_, so we want to use the same URL for displaying the author as for capturing the message from the -user. Let's rewrite our :class:`AuthorDetailView` to do that. +user. Let's rewrite our ``AuthorDetailView`` to do that. .. _REST: http://en.wikipedia.org/wiki/Representational_state_transfer @@ -423,7 +422,7 @@ code so that on ``POST`` the form gets called appropriately. .. highlightlang:: python -Our new :class:`AuthorDetail` looks like this:: +Our new ``AuthorDetail`` looks like this:: # CAUTION: you almost certainly do not want to do this. # It is provided as part of a discussion of problems you can @@ -507,10 +506,10 @@ clear division here: ``GET`` requests should get the data), and ``POST`` requests should get the :class:`FormView`. Let's set up those views first. -The :class:`AuthorDisplay` view is almost the same as :ref:`when we +The ``AuthorDisplay`` view is almost the same as :ref:`when we first introduced AuthorDetail`; we have to write our own :meth:`get_context_data()` to make the -:class:`AuthorInterestForm` available to the template. We'll skip the +``AuthorInterestForm`` available to the template. We'll skip the :meth:`get_object()` override from before for clarity. .. code-block:: python @@ -533,11 +532,11 @@ write our own :meth:`get_context_data()` to make the context.update(kwargs) return super(AuthorDisplay, self).get_context_data(**context) -Then the :class:`AuthorInterest` is a simple :class:`FormView`, but we +Then the ``AuthorInterest`` is a simple :class:`FormView`, but we have to bring in :class:`SingleObjectMixin` so we can find the author we're talking about, and we have to remember to set :attr:`template_name` to ensure that form errors will render the same -template as :class:`AuthorDisplay` is using on ``GET``. +template as ``AuthorDisplay`` is using on ``GET``. .. code-block:: python @@ -568,14 +567,14 @@ template as :class:`AuthorDisplay` is using on ``GET``. # record the interest using the message in form.cleaned_data return super(AuthorInterest, self).form_valid(form) -Finally we bring this together in a new :class:`AuthorDetail` view. We +Finally we bring this together in a new ``AuthorDetail`` view. We already know that calling :meth:`as_view()` on a class-based view gives us something that behaves exactly like a function based view, so we can do that at the point we choose between the two subviews. You can of course pass through keyword arguments to :meth:`as_view()` in the same way you would in your URLconf, such as if you wanted the -:class:`AuthorInterest` behaviour to also appear at another URL but +``AuthorInterest`` behaviour to also appear at another URL but using a different template. .. code-block:: python diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index a869b6afad..046c23bdcd 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -601,6 +601,8 @@ relation may end up filtering on different linked objects. Filters can reference fields on the model ----------------------------------------- +.. class:: F + In the examples given so far, we have constructed filters that compare the value of a model field with a constant. But what if you want to compare the value of a model field with another field on the same model? @@ -755,6 +757,8 @@ To avoid this problem, simply save the Complex lookups with Q objects ============================== +.. class:: Q + Keyword argument queries -- in :meth:`~django.db.models.query.QuerySet.filter`, etc. -- are "AND"ed together. If you need to execute more complex queries (for example, queries with ``OR`` statements), you can use ``Q`` objects. diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 7c1771b758..76849c8e23 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -37,7 +37,7 @@ display two blank forms:: Iterating over the ``formset`` will render the forms in the order they were created. You can change this order by providing an alternate implementation for -the :meth:`__iter__()` method. +the ``__iter__()`` method. Formsets can also be indexed into, which returns the corresponding form. If you override ``__iter__``, you will need to also override ``__getitem__`` to have diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 4693de6c7e..9b5794a8f2 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -300,9 +300,9 @@ loop::

    -Within this loop, ``{{ field }}`` is an instance of :class:`BoundField`. -``BoundField`` also has the following attributes, which can be useful in your -templates: +Within this loop, ``{{ field }}`` is an instance of +:class:`~django.forms.BoundField`. ``BoundField`` also has the following +attributes, which can be useful in your templates: ``{{ field.label }}`` The label of the field, e.g. ``Email address``. diff --git a/docs/topics/forms/modelforms.txt b/docs/topics/forms/modelforms.txt index 233346db0d..67d539447c 100644 --- a/docs/topics/forms/modelforms.txt +++ b/docs/topics/forms/modelforms.txt @@ -549,6 +549,8 @@ model's ``clean()`` hook. Model formsets ============== +.. class:: models.BaseModelFormSet + Like :doc:`regular formsets `, Django provides a couple of enhanced formset classes that make it easy to work with Django models. Let's reuse the ``Author`` model from above:: diff --git a/docs/topics/http/sessions.txt b/docs/topics/http/sessions.txt index baf8aa5cb5..dac146bf3e 100644 --- a/docs/topics/http/sessions.txt +++ b/docs/topics/http/sessions.txt @@ -264,7 +264,7 @@ You can edit it multiple times. - ``modification``: last modification of the session, as a :class:`~datetime.datetime` object. Defaults to the current time. - ``expiry``: expiry information for the session, as a - :class:`~datetime.datetime` object, an :class:`int` (in seconds), or + :class:`~datetime.datetime` object, an :func:`int` (in seconds), or ``None``. Defaults to the value stored in the session by :meth:`set_expiry`, if there is one, or ``None``. diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 0b13ea18be..0b37c25f18 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1248,6 +1248,8 @@ The ``set_language`` redirect view .. highlightlang:: python +.. currentmodule:: django.views.i18n + .. function:: set_language(request) As a convenience, Django comes with a view, :func:`django.views.i18n.set_language`, diff --git a/docs/topics/pagination.txt b/docs/topics/pagination.txt index 6c3ab77b11..b504b2a373 100644 --- a/docs/topics/pagination.txt +++ b/docs/topics/pagination.txt @@ -205,8 +205,8 @@ Attributes .. exception:: InvalidPage - A base class for exceptions raised when a paginator is passed an invalid - page number. + A base class for exceptions raised when a paginator is passed an invalid + page number. The :meth:`Paginator.page` method raises an exception if the requested page is invalid (i.e., not an integer) or contains no objects. Generally, it's enough diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 0627bd40d7..0548f66481 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -853,7 +853,7 @@ Normal Python unit test classes extend a base class of Hierarchy of Django unit testing classes Regardless of the version of Python you're using, if you've installed -:mod:`unittest2`, :mod:`django.utils.unittest` will point to that library. +``unittest2``, :mod:`django.utils.unittest` will point to that library. SimpleTestCase ~~~~~~~~~~~~~~ @@ -1376,7 +1376,7 @@ in the ``with`` block and reset its value to the previous state afterwards. .. function:: override_settings In case you want to override a setting for just one test method or even the -whole :class:`TestCase` class, Django provides the +whole :class:`~django.test.TestCase` class, Django provides the :func:`~django.test.utils.override_settings` decorator (see :pep:`318`). It's used like this:: -- cgit v1.3 From ebd25985962bd1466257385abcf7e8fc0df9ca0f Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 24 Dec 2012 23:14:06 +0100 Subject: Removed django.contrib.markup. --- django/contrib/markup/__init__.py | 0 django/contrib/markup/models.py | 0 django/contrib/markup/templatetags/__init__.py | 0 django/contrib/markup/templatetags/markup.py | 90 ----------------- django/contrib/markup/tests.py | 108 --------------------- .../contributing/writing-code/unit-tests.txt | 10 +- docs/ref/contrib/index.txt | 8 -- docs/ref/contrib/markup.txt | 77 --------------- docs/ref/settings.txt | 14 --- docs/ref/templates/builtins.txt | 10 -- docs/topics/security.txt | 7 -- 11 files changed, 2 insertions(+), 322 deletions(-) delete mode 100644 django/contrib/markup/__init__.py delete mode 100644 django/contrib/markup/models.py delete mode 100644 django/contrib/markup/templatetags/__init__.py delete mode 100644 django/contrib/markup/templatetags/markup.py delete mode 100644 django/contrib/markup/tests.py delete mode 100644 docs/ref/contrib/markup.txt (limited to 'docs/internals') diff --git a/django/contrib/markup/__init__.py b/django/contrib/markup/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/django/contrib/markup/models.py b/django/contrib/markup/models.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/django/contrib/markup/templatetags/__init__.py b/django/contrib/markup/templatetags/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py deleted file mode 100644 index 389c919c07..0000000000 --- a/django/contrib/markup/templatetags/markup.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Set of "markup" template filters for Django. These filters transform plain text -markup syntaxes to HTML; currently there is support for: - - * Textile, which requires the PyTextile library available at - http://loopcore.com/python-textile/ - - * Markdown, which requires the Python-markdown library from - http://www.freewisdom.org/projects/python-markdown - - * reStructuredText, which requires docutils from http://docutils.sf.net/ -""" - -from django import template -from django.conf import settings -from django.utils.encoding import force_bytes, force_text -from django.utils.safestring import mark_safe - -register = template.Library() - -@register.filter(is_safe=True) -def textile(value): - try: - import textile - except ImportError: - if settings.DEBUG: - raise template.TemplateSyntaxError("Error in 'textile' filter: The Python textile library isn't installed.") - return force_text(value) - else: - return mark_safe(force_text(textile.textile(force_bytes(value), encoding='utf-8', output='utf-8'))) - -@register.filter(is_safe=True) -def markdown(value, arg=''): - """ - Runs Markdown over a given value, optionally using various - extensions python-markdown supports. - - Syntax:: - - {{ value|markdown:"extension1_name,extension2_name..." }} - - To enable safe mode, which strips raw HTML and only returns HTML - generated by actual Markdown syntax, pass "safe" as the first - extension in the list. - - If the version of Markdown in use does not support extensions, - they will be silently ignored. - - """ - import warnings - warnings.warn('The markdown filter has been deprecated', - category=DeprecationWarning) - try: - import markdown - except ImportError: - if settings.DEBUG: - raise template.TemplateSyntaxError("Error in 'markdown' filter: The Python markdown library isn't installed.") - return force_text(value) - else: - markdown_vers = getattr(markdown, "version_info", 0) - if markdown_vers < (2, 1): - if settings.DEBUG: - raise template.TemplateSyntaxError( - "Error in 'markdown' filter: Django does not support versions of the Python markdown library < 2.1.") - return force_text(value) - else: - extensions = [e for e in arg.split(",") if e] - if extensions and extensions[0] == "safe": - extensions = extensions[1:] - return mark_safe(markdown.markdown( - force_text(value), extensions, safe_mode=True, enable_attributes=False)) - else: - return mark_safe(markdown.markdown( - force_text(value), extensions, safe_mode=False)) - -@register.filter(is_safe=True) -def restructuredtext(value): - import warnings - warnings.warn('The restructuredtext filter has been deprecated', - category=DeprecationWarning) - try: - from docutils.core import publish_parts - except ImportError: - if settings.DEBUG: - raise template.TemplateSyntaxError("Error in 'restructuredtext' filter: The Python docutils library isn't installed.") - return force_text(value) - else: - docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {}) - parts = publish_parts(source=force_bytes(value), writer_name="html4css1", settings_overrides=docutils_settings) - return mark_safe(force_text(parts["fragment"])) diff --git a/django/contrib/markup/tests.py b/django/contrib/markup/tests.py deleted file mode 100644 index 19a3b7e9d0..0000000000 --- a/django/contrib/markup/tests.py +++ /dev/null @@ -1,108 +0,0 @@ -# Quick tests for the markup templatetags (django.contrib.markup) -import re -import warnings - -from django.template import Template, Context -from django import test -from django.utils import unittest -from django.utils.html import escape - -try: - import textile -except ImportError: - textile = None - -try: - import markdown - markdown_version = getattr(markdown, "version_info", 0) -except ImportError: - markdown = None - -try: - import docutils -except ImportError: - docutils = None - -class Templates(test.TestCase): - - textile_content = """Paragraph 1 - -Paragraph 2 with "quotes" and @code@""" - - markdown_content = """Paragraph 1 - -## An h2""" - - rest_content = """Paragraph 1 - -Paragraph 2 with a link_ - -.. _link: http://www.example.com/""" - - def setUp(self): - self.save_warnings_state() - warnings.filterwarnings('ignore', category=DeprecationWarning, module='django.contrib.markup') - - def tearDown(self): - self.restore_warnings_state() - - @unittest.skipUnless(textile, 'textile not installed') - def test_textile(self): - t = Template("{% load markup %}{{ textile_content|textile }}") - rendered = t.render(Context({'textile_content':self.textile_content})).strip() - self.assertEqual(rendered.replace('\t', ''), """

    Paragraph 1

    - -

    Paragraph 2 with “quotes” and code

    """) - - @unittest.skipIf(textile, 'textile is installed') - def test_no_textile(self): - t = Template("{% load markup %}{{ textile_content|textile }}") - rendered = t.render(Context({'textile_content':self.textile_content})).strip() - self.assertEqual(rendered, escape(self.textile_content)) - - @unittest.skipUnless(markdown and markdown_version >= (2,1), 'markdown >= 2.1 not installed') - def test_markdown(self): - t = Template("{% load markup %}{{ markdown_content|markdown }}") - rendered = t.render(Context({'markdown_content':self.markdown_content})).strip() - pattern = re.compile("""

    Paragraph 1\s*

    \s*

    \s*An h2

    """) - self.assertTrue(pattern.match(rendered)) - - @unittest.skipUnless(markdown and markdown_version >= (2,1), 'markdown >= 2.1 not installed') - def test_markdown_attribute_disable(self): - t = Template("{% load markup %}{{ markdown_content|markdown:'safe' }}") - markdown_content = "{@onclick=alert('hi')}some paragraph" - rendered = t.render(Context({'markdown_content':markdown_content})).strip() - self.assertTrue('@' in rendered) - - @unittest.skipUnless(markdown and markdown_version >= (2,1), 'markdown >= 2.1 not installed') - def test_markdown_attribute_enable(self): - t = Template("{% load markup %}{{ markdown_content|markdown }}") - markdown_content = "{@onclick=alert('hi')}some paragraph" - rendered = t.render(Context({'markdown_content':markdown_content})).strip() - self.assertFalse('@' in rendered) - - @unittest.skipIf(markdown, 'markdown is installed') - def test_no_markdown(self): - t = Template("{% load markup %}{{ markdown_content|markdown }}") - rendered = t.render(Context({'markdown_content':self.markdown_content})).strip() - self.assertEqual(rendered, self.markdown_content) - - @unittest.skipUnless(docutils, 'docutils not installed') - def test_docutils(self): - t = Template("{% load markup %}{{ rest_content|restructuredtext }}") - rendered = t.render(Context({'rest_content':self.rest_content})).strip() - # Different versions of docutils return slightly different HTML - try: - # Docutils v0.4 and earlier - self.assertEqual(rendered, """

    Paragraph 1

    -

    Paragraph 2 with a link

    """) - except AssertionError: - # Docutils from SVN (which will become 0.5) - self.assertEqual(rendered, """

    Paragraph 1

    -

    Paragraph 2 with a link

    """) - - @unittest.skipIf(docutils, 'docutils is installed') - def test_no_docutils(self): - t = Template("{% load markup %}{{ rest_content|restructuredtext }}") - rendered = t.render(Context({'rest_content':self.rest_content})).strip() - self.assertEqual(rendered, self.rest_content) diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index afef554a8c..a03951d141 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -145,9 +145,6 @@ If you want to run the full suite of tests, you'll need to install a number of dependencies: * PyYAML_ -* Markdown_ -* Textile_ -* Docutils_ * setuptools_ * memcached_, plus a :ref:`supported Python binding ` * gettext_ (:ref:`gettext_on_windows`) @@ -160,9 +157,6 @@ Each of these dependencies is optional. If you're missing any of them, the associated tests will be skipped. .. _PyYAML: http://pyyaml.org/wiki/PyYAML -.. _Markdown: http://pypi.python.org/pypi/Markdown/1.7 -.. _Textile: http://pypi.python.org/pypi/textile -.. _docutils: http://pypi.python.org/pypi/docutils/0.4 .. _setuptools: http://pypi.python.org/pypi/setuptools/ .. _memcached: http://memcached.org/ .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html @@ -200,7 +194,7 @@ multiple modules by using a ``tests`` directory in the normal Python way. For the tests to be found, a ``models.py`` file must exist, even if it's empty. If you have URLs that need to be mapped, put them in ``tests/urls.py``. -To run tests for just one contrib app (e.g. ``markup``), use the same +To run tests for just one contrib app (e.g. ``auth``), use the same method as above:: - ./runtests.py --settings=settings markup + ./runtests.py --settings=settings auth diff --git a/docs/ref/contrib/index.txt b/docs/ref/contrib/index.txt index d042fd96ca..e5cea01ead 100644 --- a/docs/ref/contrib/index.txt +++ b/docs/ref/contrib/index.txt @@ -31,7 +31,6 @@ those packages have. formtools/index gis/index humanize - markup messages redirects sitemaps @@ -121,13 +120,6 @@ A set of Django template filters useful for adding a "human touch" to data. See the :doc:`humanize documentation `. -markup -====== - -A collection of template filters that implement common markup languages - -See the :doc:`markup documentation `. - messages ======== diff --git a/docs/ref/contrib/markup.txt b/docs/ref/contrib/markup.txt deleted file mode 100644 index 9215c64f93..0000000000 --- a/docs/ref/contrib/markup.txt +++ /dev/null @@ -1,77 +0,0 @@ -===================== -django.contrib.markup -===================== - -.. module:: django.contrib.markup - :synopsis: A collection of template filters that implement common markup languages. - -.. deprecated:: 1.5 - This module has been deprecated. - -Django provides template filters that implement the following markup -languages: - -* ``textile`` -- implements `Textile`_ -- requires `PyTextile`_ -* ``markdown`` -- implements `Markdown`_ -- requires `Python-markdown`_ (>=2.1) -* ``restructuredtext`` -- implements `reST (reStructured Text)`_ - -- requires `doc-utils`_ - -In each case, the filter expects formatted markup as a string and -returns a string representing the marked-up text. For example, the -``textile`` filter converts text that is marked-up in Textile format -to HTML. - -To activate these filters, add ``'django.contrib.markup'`` to your -:setting:`INSTALLED_APPS` setting. Once you've done that, use -``{% load markup %}`` in a template, and you'll have access to these filters. -For more documentation, read the source code in -:file:`django/contrib/markup/templatetags/markup.py`. - -.. warning:: - - The output of markup filters is marked "safe" and will not be escaped when - rendered in a template. Always be careful to sanitize your inputs and make - sure you are not leaving yourself vulnerable to cross-site scripting or - other types of attacks. - -.. _Textile: http://en.wikipedia.org/wiki/Textile_%28markup_language%29 -.. _Markdown: http://en.wikipedia.org/wiki/Markdown -.. _reST (reStructured Text): http://en.wikipedia.org/wiki/ReStructuredText -.. _PyTextile: http://loopcore.com/python-textile/ -.. _Python-markdown: http://pypi.python.org/pypi/Markdown -.. _doc-utils: http://docutils.sf.net/ - -reStructured Text ------------------ - -When using the ``restructuredtext`` markup filter you can define a -:setting:`RESTRUCTUREDTEXT_FILTER_SETTINGS` in your django settings to -override the default writer settings. See the `restructuredtext writer -settings`_ for details on what these settings are. - -.. warning:: - - reStructured Text has features that allow raw HTML to be included, and that - allow arbitrary files to be included. These can lead to XSS vulnerabilities - and leaking of private information. It is your responsibility to check the - features of this library and configure appropriately to avoid this. See the - `Deploying Docutils Securely - `_ documentation. - -.. _restructuredtext writer settings: http://docutils.sourceforge.net/docs/user/config.html#html4css1-writer - -Markdown --------- - -The Python Markdown library supports options named "safe_mode" and -"enable_attributes". Both relate to the security of the output. To enable both -options in tandem, the markdown filter supports the "safe" argument:: - - {{ markdown_content_var|markdown:"safe" }} - -.. warning:: - - Versions of the Python-Markdown library prior to 2.1 do not support the - optional disabling of attributes. This is a security flaw. Therefore, - ``django.contrib.markup`` has dropped support for versions of - Python-Markdown < 2.1 in Django 1.5. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 5815062266..bcc2a461c7 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1502,20 +1502,6 @@ Default: ``()`` (Empty tuple) A tuple of profanities, as strings, that will be forbidden in comments when ``COMMENTS_ALLOW_PROFANITIES`` is ``False``. -.. setting:: RESTRUCTUREDTEXT_FILTER_SETTINGS - -RESTRUCTUREDTEXT_FILTER_SETTINGS --------------------------------- - -Default: ``{}`` - -A dictionary containing settings for the ``restructuredtext`` markup filter from -the :doc:`django.contrib.markup application `. They override -the default writer settings. See the Docutils restructuredtext `writer settings -docs`_ for details. - -.. _writer settings docs: http://docutils.sourceforge.net/docs/user/config.html#html4css1-writer - .. setting:: ROOT_URLCONF ROOT_URLCONF diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 4bbc839bea..867d1e5cc0 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -2356,16 +2356,6 @@ django.contrib.humanize A set of Django template filters useful for adding a "human touch" to data. See :doc:`/ref/contrib/humanize`. -django.contrib.markup -^^^^^^^^^^^^^^^^^^^^^ - -A collection of template filters that implement these common markup languages: - -* Textile -* Markdown -* reST (reStructuredText) - -See the :doc:`markup documentation `. django.contrib.webdesign ^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/security.txt b/docs/topics/security.txt index 9c4c4bbd9e..07b8ebcdd2 100644 --- a/docs/topics/security.txt +++ b/docs/topics/security.txt @@ -48,13 +48,6 @@ escaping. You should also be very careful when storing HTML in the database, especially when that HTML is retrieved and displayed. -Markup library --------------- - -If you use :mod:`django.contrib.markup`, you need to ensure that the filters are -only used on trusted input, or that you have correctly configured them to ensure -they do not allow raw HTML output. See the documentation of that module for more -information. Cross site request forgery (CSRF) protection ============================================ -- cgit v1.3 From a04df803a590c5bffd9437d9199bc0107ba0e966 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Sat, 29 Dec 2012 18:52:50 -0500 Subject: Removed links to deprecated IGNORABLE_404_STARTS/ENDS settings. refs #19516 and 641acf76e7 --- docs/internals/deprecation.txt | 6 +++--- docs/releases/1.4-alpha-1.txt | 17 +++++++++-------- docs/releases/1.4-beta-1.txt | 17 +++++++++-------- docs/releases/1.4.txt | 17 +++++++++-------- 4 files changed, 30 insertions(+), 27 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 74f544c220..c976f5a880 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -227,9 +227,9 @@ these changes. be accessible through their GB-prefixed names (GB is the correct ISO 3166 code for United Kingdom). -* The :setting:`IGNORABLE_404_STARTS` and :setting:`IGNORABLE_404_ENDS` - settings have been superseded by :setting:`IGNORABLE_404_URLS` in - the 1.4 release. They will be removed. +* The ``IGNORABLE_404_STARTS`` and ``IGNORABLE_404_ENDS`` settings have been + superseded by :setting:`IGNORABLE_404_URLS` in the 1.4 release. They will be + removed. * The :doc:`form wizard ` has been refactored to use class-based views with pluggable backends in 1.4. diff --git a/docs/releases/1.4-alpha-1.txt b/docs/releases/1.4-alpha-1.txt index fc19e90384..4086cfdecc 100644 --- a/docs/releases/1.4-alpha-1.txt +++ b/docs/releases/1.4-alpha-1.txt @@ -813,11 +813,12 @@ For more details, see the documentation about Until Django 1.3, it was possible to exclude some URLs from Django's :doc:`404 error reporting` by adding prefixes to -:setting:`IGNORABLE_404_STARTS` and suffixes to :setting:`IGNORABLE_404_ENDS`. +``IGNORABLE_404_STARTS`` and suffixes to ``IGNORABLE_404_ENDS``. In Django 1.4, these two settings are superseded by -:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular expressions. -Django won't send an email for 404 errors on URLs that match any of them. +:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular +expressions. Django won't send an email for 404 errors on URLs that match any +of them. Furthermore, the previous settings had some rather arbitrary default values:: @@ -827,12 +828,12 @@ Furthermore, the previous settings had some rather arbitrary default values:: It's not Django's role to decide if your website has a legacy ``/cgi-bin/`` section or a ``favicon.ico``. As a consequence, the default values of -:setting:`IGNORABLE_404_URLS`, :setting:`IGNORABLE_404_STARTS` and -:setting:`IGNORABLE_404_ENDS` are all now empty. +:setting:`IGNORABLE_404_URLS`, ``IGNORABLE_404_STARTS``, and +``IGNORABLE_404_ENDS`` are all now empty. -If you have customized :setting:`IGNORABLE_404_STARTS` or -:setting:`IGNORABLE_404_ENDS`, or if you want to keep the old default value, -you should add the following lines in your settings file:: +If you have customized ``IGNORABLE_404_STARTS`` or ``IGNORABLE_404_ENDS``, or +if you want to keep the old default value, you should add the following lines +in your settings file:: import re IGNORABLE_404_URLS = ( diff --git a/docs/releases/1.4-beta-1.txt b/docs/releases/1.4-beta-1.txt index 2c84d21b8d..a8732a9e65 100644 --- a/docs/releases/1.4-beta-1.txt +++ b/docs/releases/1.4-beta-1.txt @@ -881,11 +881,12 @@ For more details, see the documentation about Until Django 1.3, it was possible to exclude some URLs from Django's :doc:`404 error reporting` by adding prefixes to -:setting:`IGNORABLE_404_STARTS` and suffixes to :setting:`IGNORABLE_404_ENDS`. +``IGNORABLE_404_STARTS`` and suffixes to ``IGNORABLE_404_ENDS``. In Django 1.4, these two settings are superseded by -:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular expressions. -Django won't send an email for 404 errors on URLs that match any of them. +:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular +expressions. Django won't send an email for 404 errors on URLs that match any +of them. Furthermore, the previous settings had some rather arbitrary default values:: @@ -895,12 +896,12 @@ Furthermore, the previous settings had some rather arbitrary default values:: It's not Django's role to decide if your website has a legacy ``/cgi-bin/`` section or a ``favicon.ico``. As a consequence, the default values of -:setting:`IGNORABLE_404_URLS`, :setting:`IGNORABLE_404_STARTS` and -:setting:`IGNORABLE_404_ENDS` are all now empty. +:setting:`IGNORABLE_404_URLS`, ``IGNORABLE_404_STARTS``, and +``IGNORABLE_404_ENDS`` are all now empty. -If you have customized :setting:`IGNORABLE_404_STARTS` or -:setting:`IGNORABLE_404_ENDS`, or if you want to keep the old default value, -you should add the following lines in your settings file:: +If you have customized ``IGNORABLE_404_STARTS`` or ``IGNORABLE_404_ENDS``, or +if you want to keep the old default value, you should add the following lines +in your settings file:: import re IGNORABLE_404_URLS = ( diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index d700ba8b89..cf53b37f17 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -966,11 +966,12 @@ For more details, see the documentation about Until Django 1.3, it was possible to exclude some URLs from Django's :doc:`404 error reporting` by adding prefixes to -:setting:`IGNORABLE_404_STARTS` and suffixes to :setting:`IGNORABLE_404_ENDS`. +``IGNORABLE_404_STARTS`` and suffixes to ``IGNORABLE_404_ENDS``. In Django 1.4, these two settings are superseded by -:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular expressions. -Django won't send an email for 404 errors on URLs that match any of them. +:setting:`IGNORABLE_404_URLS`, which is a list of compiled regular +expressions. Django won't send an email for 404 errors on URLs that match any +of them. Furthermore, the previous settings had some rather arbitrary default values:: @@ -980,12 +981,12 @@ Furthermore, the previous settings had some rather arbitrary default values:: It's not Django's role to decide if your website has a legacy ``/cgi-bin/`` section or a ``favicon.ico``. As a consequence, the default values of -:setting:`IGNORABLE_404_URLS`, :setting:`IGNORABLE_404_STARTS` and -:setting:`IGNORABLE_404_ENDS` are all now empty. +:setting:`IGNORABLE_404_URLS`, ``IGNORABLE_404_STARTS``, and +``IGNORABLE_404_ENDS`` are all now empty. -If you have customized :setting:`IGNORABLE_404_STARTS` or -:setting:`IGNORABLE_404_ENDS`, or if you want to keep the old default value, -you should add the following lines in your settings file:: +If you have customized ``IGNORABLE_404_STARTS`` or ``IGNORABLE_404_ENDS``, or +if you want to keep the old default value, you should add the following lines +in your settings file:: import re IGNORABLE_404_URLS = ( -- cgit v1.3 From 9b5f64cc6ed5f1e904093fe4e6ff0f681b8e545f Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 1 Jan 2013 08:12:42 -0500 Subject: Fixed #19516 - Fixed remaining broken links. Added -n to sphinx builds to catch issues going forward. --- docs/Makefile | 2 +- docs/faq/usage.txt | 9 +- docs/howto/custom-model-fields.txt | 18 +- docs/howto/custom-template-tags.txt | 6 + .../contributing/writing-code/coding-style.txt | 6 +- docs/internals/deprecation.txt | 20 +-- docs/intro/tutorial01.txt | 4 +- docs/intro/tutorial04.txt | 8 +- docs/make.bat | 2 +- docs/ref/class-based-views/base.txt | 34 ++-- docs/ref/class-based-views/flattened-index.txt | 78 ++++----- docs/ref/class-based-views/generic-date-based.txt | 2 +- docs/ref/class-based-views/generic-display.txt | 40 ++--- docs/ref/class-based-views/generic-editing.txt | 10 +- docs/ref/class-based-views/mixins-date-based.txt | 9 +- docs/ref/class-based-views/mixins-editing.txt | 43 +++-- .../class-based-views/mixins-multiple-object.txt | 22 ++- docs/ref/class-based-views/mixins-simple.txt | 12 +- .../ref/class-based-views/mixins-single-object.txt | 41 +++-- docs/ref/clickjacking.txt | 8 +- docs/ref/contrib/admin/admindocs.txt | 2 +- docs/ref/contrib/admin/index.txt | 26 ++- docs/ref/contrib/comments/custom.txt | 26 +-- docs/ref/contrib/comments/example.txt | 4 +- docs/ref/contrib/comments/moderation.txt | 9 +- docs/ref/contrib/comments/signals.txt | 4 +- docs/ref/contrib/contenttypes.txt | 8 +- docs/ref/contrib/flatpages.txt | 4 +- docs/ref/contrib/formtools/form-preview.txt | 16 +- docs/ref/contrib/formtools/form-wizard.txt | 32 ++-- docs/ref/contrib/formtools/index.txt | 2 + docs/ref/contrib/gis/db-api.txt | 17 +- docs/ref/contrib/gis/feeds.txt | 10 +- docs/ref/contrib/gis/geoquerysets.txt | 4 +- docs/ref/contrib/gis/geos.txt | 14 +- docs/ref/contrib/gis/install/index.txt | 2 +- docs/ref/contrib/gis/tutorial.txt | 14 +- docs/ref/contrib/sitemaps.txt | 29 ++-- docs/ref/contrib/staticfiles.txt | 10 +- docs/ref/contrib/syndication.txt | 2 +- docs/ref/databases.txt | 4 +- docs/ref/django-admin.txt | 2 + docs/ref/exceptions.txt | 15 ++ docs/ref/files/file.txt | 4 +- docs/ref/files/storage.txt | 2 +- docs/ref/forms/api.txt | 41 ++--- docs/ref/forms/widgets.txt | 12 +- docs/ref/middleware.txt | 4 +- docs/ref/models/fields.txt | 93 ++++++---- docs/ref/models/options.txt | 2 +- docs/ref/request-response.txt | 2 +- docs/ref/settings.txt | 2 +- docs/ref/signals.txt | 11 +- docs/ref/template-response.txt | 24 ++- docs/ref/templates/api.txt | 22 ++- docs/ref/templates/builtins.txt | 2 +- docs/ref/urls.txt | 2 - docs/ref/utils.txt | 19 +- docs/ref/validators.txt | 2 +- docs/releases/1.2-beta-1.txt | 2 +- docs/releases/1.2.txt | 10 +- docs/releases/1.3-alpha-1.txt | 2 +- docs/releases/1.3-beta-1.txt | 6 +- docs/releases/1.3.txt | 5 +- docs/releases/1.4-alpha-1.txt | 2 +- docs/releases/1.4-beta-1.txt | 2 +- docs/releases/1.4.txt | 4 +- docs/topics/auth/passwords.txt | 18 +- docs/topics/cache.txt | 8 +- docs/topics/class-based-views/generic-display.txt | 12 +- docs/topics/class-based-views/generic-editing.txt | 81 +++++---- docs/topics/class-based-views/mixins.txt | 192 +++++++++++---------- docs/topics/db/sql.txt | 5 +- docs/topics/db/transactions.txt | 7 +- docs/topics/forms/formsets.txt | 2 + docs/topics/http/file-uploads.txt | 4 +- docs/topics/http/views.txt | 2 + docs/topics/i18n/timezones.txt | 4 +- docs/topics/logging.txt | 12 +- docs/topics/python3.txt | 73 ++++---- docs/topics/serialization.txt | 4 +- docs/topics/settings.txt | 3 +- docs/topics/testing/overview.txt | 8 +- 83 files changed, 729 insertions(+), 613 deletions(-) (limited to 'docs/internals') diff --git a/docs/Makefile b/docs/Makefile index f6293a8e7f..2a8bcd7101 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,7 +10,7 @@ BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -n -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . diff --git a/docs/faq/usage.txt b/docs/faq/usage.txt index 151454398d..be3839e08f 100644 --- a/docs/faq/usage.txt +++ b/docs/faq/usage.txt @@ -52,10 +52,11 @@ Using a :class:`~django.db.models.FileField` or an #. All that will be stored in your database is a path to the file (relative to :setting:`MEDIA_ROOT`). You'll most likely want to use the - convenience :attr:`~django.core.files.File.url` attribute provided by - Django. For example, if your :class:`~django.db.models.ImageField` is - called ``mug_shot``, you can get the absolute path to your image in a - template with ``{{ object.mug_shot.url }}``. + convenience :attr:`~django.db.models.fields.files.FieldFile.url` attribute + provided by Django. For example, if your + :class:`~django.db.models.ImageField` is called ``mug_shot``, you can get + the absolute path to your image in a template with + ``{{ object.mug_shot.url }}``. How do I make a variable available to all my templates? ------------------------------------------------------- diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index e3dae840fc..7b5fe6349e 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -199,20 +199,20 @@ The :meth:`~django.db.models.Field.__init__` method takes the following parameters: * :attr:`~django.db.models.Field.verbose_name` -* :attr:`~django.db.models.Field.name` +* ``name`` * :attr:`~django.db.models.Field.primary_key` -* :attr:`~django.db.models.Field.max_length` +* :attr:`~django.db.models.CharField.max_length` * :attr:`~django.db.models.Field.unique` * :attr:`~django.db.models.Field.blank` * :attr:`~django.db.models.Field.null` * :attr:`~django.db.models.Field.db_index` -* :attr:`~django.db.models.Field.rel`: Used for related fields (like - :class:`ForeignKey`). For advanced use only. +* ``rel``: Used for related fields (like :class:`ForeignKey`). For advanced + use only. * :attr:`~django.db.models.Field.default` * :attr:`~django.db.models.Field.editable` -* :attr:`~django.db.models.Field.serialize`: If ``False``, the field will - not be serialized when the model is passed to Django's :doc:`serializers - `. Defaults to ``True``. +* ``serialize``: If ``False``, the field will not be serialized when the model + is passed to Django's :doc:`serializers `. Defaults to + ``True``. * :attr:`~django.db.models.Field.unique_for_date` * :attr:`~django.db.models.Field.unique_for_month` * :attr:`~django.db.models.Field.unique_for_year` @@ -222,7 +222,7 @@ parameters: * :attr:`~django.db.models.Field.db_tablespace`: Only for index creation, if the backend supports :doc:`tablespaces `. You can usually ignore this option. -* :attr:`~django.db.models.Field.auto_created`: True if the field was +* ``auto_created``: True if the field was automatically created, as for the `OneToOneField` used by model inheritance. For advanced use only. @@ -443,7 +443,7 @@ Python object type we want to store in the model's attribute. If anything is going wrong during value conversion, you should raise a :exc:`~django.core.exceptions.ValidationError` exception. -**Remember:** If your custom field needs the :meth:`to_python` method to be +**Remember:** If your custom field needs the :meth:`.to_python` method to be called when it is created, you should be using `The SubfieldBase metaclass`_ mentioned earlier. Otherwise :meth:`.to_python` won't be called automatically. diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 31fbc9e96c..0d35654a04 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -114,6 +114,8 @@ your function. Example: Registering custom filters ~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. method:: django.template.Library.filter + Once you've written your filter definition, you need to register it with your ``Library`` instance, to make it available to Django's template language: @@ -151,6 +153,8 @@ are described in :ref:`filters and auto-escaping ` and Template filters that expect strings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. method:: django.template.defaultfilters.stringfilter + If you're writing a template filter that only expects a string as the first argument, you should use the decorator ``stringfilter``. This will convert an object to its string value before being passed to your function: @@ -700,6 +704,8 @@ cannot resolve the string passed to it in the current context of the page. Simple tags ~~~~~~~~~~~ +.. method:: django.template.Library.simple_tag + Many template tags take a number of arguments -- strings or template variables -- and return a string after doing some processing based solely on the input arguments and some external information. For example, the diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt index a699e39bd8..0d84cdac9a 100644 --- a/docs/internals/contributing/writing-code/coding-style.txt +++ b/docs/internals/contributing/writing-code/coding-style.txt @@ -177,9 +177,9 @@ That means that the ability for third parties to import the module at the top level is incompatible with the ability to configure the settings object manually, or makes it very difficult in some circumstances. -Instead of the above code, a level of laziness or indirection must be used, such -as :class:`django.utils.functional.LazyObject`, -:func:`django.utils.functional.lazy` or ``lambda``. +Instead of the above code, a level of laziness or indirection must be used, +such as ``django.utils.functional.LazyObject``, +``django.utils.functional.lazy()`` or ``lambda``. Miscellaneous ------------- diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index c976f5a880..faa6d1ff02 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -167,9 +167,8 @@ these changes. * ``django.core.context_processors.PermWrapper`` and ``django.core.context_processors.PermLookupDict`` will be removed in favor of the corresponding - :class:`django.contrib.auth.context_processors.PermWrapper` and - :class:`django.contrib.auth.context_processors.PermLookupDict`, - respectively. + ``django.contrib.auth.context_processors.PermWrapper`` and + ``django.contrib.auth.context_processors.PermLookupDict``, respectively. * The :setting:`MEDIA_URL` or :setting:`STATIC_URL` settings will be required to end with a trailing slash to ensure there is a consistent @@ -218,10 +217,10 @@ these changes. synonym for ``django.views.decorators.csrf.csrf_exempt``, which should be used to replace it. -* The :class:`~django.core.cache.backends.memcached.CacheClass` backend +* The ``django.core.cache.backends.memcached.CacheClass`` backend was split into two in Django 1.3 in order to introduce support for - PyLibMC. The historical :class:`~django.core.cache.backends.memcached.CacheClass` - will be removed in favor of :class:`~django.core.cache.backends.memcached.MemcachedCache`. + PyLibMC. The historical ``CacheClass`` will be removed in favor of + ``django.core.cache.backends.memcached.MemcachedCache``. * The UK-prefixed objects of ``django.contrib.localflavor.uk`` will only be accessible through their GB-prefixed names (GB is the correct @@ -243,8 +242,8 @@ these changes. :setting:`LOGGING` setting should include this filter explicitly if it is desired. -* The builtin truncation functions :func:`django.utils.text.truncate_words` - and :func:`django.utils.text.truncate_html_words` will be removed in +* The builtin truncation functions ``django.utils.text.truncate_words()`` + and ``django.utils.text.truncate_html_words()`` will be removed in favor of the ``django.utils.text.Truncator`` class. * The :class:`~django.contrib.gis.geoip.GeoIP` class was moved to @@ -257,9 +256,8 @@ these changes. :data:`~django.conf.urls.handler500`, are now available through :mod:`django.conf.urls` . -* The functions :func:`~django.core.management.setup_environ` and - :func:`~django.core.management.execute_manager` will be removed from - :mod:`django.core.management`. This also means that the old (pre-1.4) +* The functions ``setup_environ()`` and ``execute_manager()`` will be removed + from :mod:`django.core.management`. This also means that the old (pre-1.4) style of :file:`manage.py` file will no longer work. * Setting the ``is_safe`` and ``needs_autoescape`` flags as attributes of diff --git a/docs/intro/tutorial01.txt b/docs/intro/tutorial01.txt index ab6c8b999f..632f27f2d2 100644 --- a/docs/intro/tutorial01.txt +++ b/docs/intro/tutorial01.txt @@ -369,8 +369,8 @@ its human-readable name. Some :class:`~django.db.models.Field` classes have required elements. :class:`~django.db.models.CharField`, for example, requires that you give it a -:attr:`~django.db.models.Field.max_length`. That's used not only in the database -schema, but in validation, as we'll soon see. +:attr:`~django.db.models.CharField.max_length`. That's used not only in the +database schema, but in validation, as we'll soon see. Finally, note a relationship is defined, using :class:`~django.db.models.ForeignKey`. That tells Django each ``Choice`` is related diff --git a/docs/intro/tutorial04.txt b/docs/intro/tutorial04.txt index 333ef9fbc3..f047067aa7 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -234,12 +234,12 @@ two views abstract the concepts of "display a list of objects" and * Each generic view needs to know what model it will be acting upon. This is provided using the ``model`` parameter. -* The :class:`~django.views.generic.list.DetailView` generic view +* The :class:`~django.views.generic.detail.DetailView` generic view expects the primary key value captured from the URL to be called ``"pk"``, so we've changed ``poll_id`` to ``pk`` for the generic views. -By default, the :class:`~django.views.generic.list.DetailView` generic +By default, the :class:`~django.views.generic.detail.DetailView` generic view uses a template called ``/_detail.html``. In our case, it'll use the template ``"polls/poll_detail.html"``. The ``template_name`` argument is used to tell Django to use a specific @@ -247,7 +247,7 @@ template name instead of the autogenerated default template name. We also specify the ``template_name`` for the ``results`` list view -- this ensures that the results view and the detail view have a different appearance when rendered, even though they're both a -:class:`~django.views.generic.list.DetailView` behind the scenes. +:class:`~django.views.generic.detail.DetailView` behind the scenes. Similarly, the :class:`~django.views.generic.list.ListView` generic view uses a default template called ``/_list.html``; we use ``template_name`` to tell In previous parts of the tutorial, the templates have been provided with a context that contains the ``poll`` and ``latest_poll_list`` -context variables. For DetailView the ``poll`` variable is provided +context variables. For ``DetailView`` the ``poll`` variable is provided automatically -- since we're using a Django model (``Poll``), Django is able to determine an appropriate name for the context variable. However, for ListView, the automatically generated context variable is diff --git a/docs/make.bat b/docs/make.bat index d7f54b2059..65602aa160 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -6,7 +6,7 @@ if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set ALLSPHINXOPTS=-n -d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% diff --git a/docs/ref/class-based-views/base.txt b/docs/ref/class-based-views/base.txt index cc9aa852f1..c070ea707a 100644 --- a/docs/ref/class-based-views/base.txt +++ b/docs/ref/class-based-views/base.txt @@ -49,9 +49,13 @@ View **Attributes** - .. attribute:: http_method_names = ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] + .. attribute:: http_method_names - The default list of HTTP method names that this view will accept. + The list of HTTP method names that this view will accept. + + Default:: + + ['get', 'post', 'put', 'delete', 'head', 'options', 'trace'] **Methods** @@ -68,12 +72,11 @@ View The default implementation will inspect the HTTP method and attempt to delegate to a method that matches the HTTP method; a ``GET`` will be - delegated to :meth:`~View.get()`, a ``POST`` to :meth:`~View.post()`, - and so on. + delegated to ``get()``, a ``POST`` to ``post()``, and so on. - By default, a ``HEAD`` request will be delegated to :meth:`~View.get()`. + By default, a ``HEAD`` request will be delegated to ``get()``. If you need to handle ``HEAD`` requests in a different way than ``GET``, - you can override the :meth:`~View.head()` method. See + you can override the ``head()`` method. See :ref:`supporting-other-http-methods` for an example. The default implementation also sets ``request``, ``args`` and @@ -111,9 +114,9 @@ TemplateView **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` - 3. :meth:`get_context_data()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` + 3. :meth:`~django.views.generic.base.ContextMixin.get_context_data()` **Example views.py**:: @@ -169,8 +172,8 @@ RedirectView **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` 3. :meth:`get_redirect_url()` **Example views.py**:: @@ -230,9 +233,8 @@ RedirectView Constructs the target URL for redirection. - The default implementation uses :attr:`~RedirectView.url` as a starting + The default implementation uses :attr:`url` as a starting string, performs expansion of ``%`` parameters in that string, as well - as the appending of query string if requested by - :attr:`~RedirectView.query_string`. Subclasses may implement any - behavior they wish, as long as the method returns a redirect-ready URL - string. + as the appending of query string if requested by :attr:`query_string`. + Subclasses may implement any behavior they wish, as long as the method + returns a redirect-ready URL string. diff --git a/docs/ref/class-based-views/flattened-index.txt b/docs/ref/class-based-views/flattened-index.txt index aa2f51f156..2e75363c58 100644 --- a/docs/ref/class-based-views/flattened-index.txt +++ b/docs/ref/class-based-views/flattened-index.txt @@ -23,7 +23,7 @@ View * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` TemplateView @@ -40,9 +40,9 @@ TemplateView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.base.TemplateView.get` -* :meth:`~django.views.generic.base.TemplateView.get_context_data` -* :meth:`~django.views.generic.base.View.head` +* ``get()`` +* :meth:`~django.views.generic.base.ContextMixin.get_context_data` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -60,15 +60,15 @@ RedirectView **Methods** * :meth:`~django.views.generic.base.View.as_view` -* :meth:`~django.views.generic.base.RedirectView.delete` +* ``delete()`` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.base.RedirectView.get` +* ``get()`` * :meth:`~django.views.generic.base.RedirectView.get_redirect_url` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` -* :meth:`~django.views.generic.base.RedirectView.options` -* :meth:`~django.views.generic.base.RedirectView.post` -* :meth:`~django.views.generic.base.RedirectView.put` +* ``options()`` +* ``post()`` +* ``put()`` Detail Views ------------ @@ -95,10 +95,10 @@ DetailView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.detail.BaseDetailView.get` +* ``get()`` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -130,7 +130,7 @@ ListView * :meth:`~django.views.generic.list.BaseListView.get` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -161,10 +161,10 @@ FormView * :meth:`~django.views.generic.edit.FormMixin.get_context_data` * :meth:`~django.views.generic.edit.FormMixin.get_form` * :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` -* :meth:`~django.views.generic.edit.ProcessFormView.post` -* :meth:`~django.views.generic.edit.ProcessFormView.put` +* ``post()`` +* ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` CreateView @@ -199,10 +199,10 @@ CreateView * :meth:`~django.views.generic.edit.FormMixin.get_form` * :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.edit.ProcessFormView.post` -* :meth:`~django.views.generic.edit.ProcessFormView.put` +* ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` UpdateView @@ -237,10 +237,10 @@ UpdateView * :meth:`~django.views.generic.edit.FormMixin.get_form` * :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.edit.ProcessFormView.post` -* :meth:`~django.views.generic.edit.ProcessFormView.put` +* ``put()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` DeleteView @@ -265,14 +265,14 @@ DeleteView **Methods** * :meth:`~django.views.generic.base.View.as_view` -* :meth:`~django.views.generic.edit.DeletionMixin.delete` +* ``delete()`` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.detail.BaseDetailView.get` +* ``get()`` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` -* :meth:`~django.views.generic.edit.DeletionMixin.post` +* ``post()`` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` Date-based views @@ -302,13 +302,13 @@ ArchiveIndexView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -324,7 +324,7 @@ YearArchiveView * :attr:`~django.views.generic.list.MultipleObjectMixin.context_object_name` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name`] * :attr:`~django.views.generic.dates.DateMixin.date_field` [:meth:`~django.views.generic.dates.DateMixin.get_date_field`] * :attr:`~django.views.generic.base.View.http_method_names` -* :attr:`~django.views.generic.dates.BaseYearArchiveView.make_object_list` [:meth:`~django.views.generic.dates.BaseYearArchiveView.get_make_object_list`] +* :attr:`~django.views.generic.dates.YearArchiveView.make_object_list` [:meth:`~django.views.generic.dates.YearArchiveView.get_make_object_list`] * :attr:`~django.views.generic.list.MultipleObjectMixin.model` * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_by` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_by`] * :attr:`~django.views.generic.list.MultipleObjectMixin.paginate_orphans` [:meth:`~django.views.generic.list.MultipleObjectMixin.get_paginate_orphans`] @@ -340,13 +340,13 @@ YearArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -379,7 +379,7 @@ MonthArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` @@ -387,7 +387,7 @@ MonthArchiveView * :meth:`~django.views.generic.dates.MonthMixin.get_next_month` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -420,13 +420,13 @@ WeekArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_queryset` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -461,7 +461,7 @@ DayArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` @@ -471,7 +471,7 @@ DayArchiveView * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` * :meth:`~django.views.generic.dates.DayMixin.get_previous_day` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -506,7 +506,7 @@ TodayArchiveView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.dates.BaseDateListView.get` +* ``get()`` * :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.BaseDateListView.get_date_list` * :meth:`~django.views.generic.dates.BaseDateListView.get_dated_items` @@ -516,7 +516,7 @@ TodayArchiveView * :meth:`~django.views.generic.list.MultipleObjectMixin.get_paginator` * :meth:`~django.views.generic.dates.DayMixin.get_previous_day` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` @@ -551,13 +551,13 @@ DateDetailView * :meth:`~django.views.generic.base.View.as_view` * :meth:`~django.views.generic.base.View.dispatch` -* :meth:`~django.views.generic.detail.BaseDetailView.get` +* ``get()`` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data` * :meth:`~django.views.generic.dates.DayMixin.get_next_day` * :meth:`~django.views.generic.dates.MonthMixin.get_next_month` * :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` * :meth:`~django.views.generic.dates.DayMixin.get_previous_day` * :meth:`~django.views.generic.dates.MonthMixin.get_previous_month` -* :meth:`~django.views.generic.base.View.head` +* ``head()`` * :meth:`~django.views.generic.base.View.http_method_not_allowed` * :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` diff --git a/docs/ref/class-based-views/generic-date-based.txt b/docs/ref/class-based-views/generic-date-based.txt index 0ae0bcdf42..42dbab4dd8 100644 --- a/docs/ref/class-based-views/generic-date-based.txt +++ b/docs/ref/class-based-views/generic-date-based.txt @@ -580,7 +580,7 @@ DateDetailView * :class:`django.views.generic.dates.MonthMixin` * :class:`django.views.generic.dates.DayMixin` * :class:`django.views.generic.dates.DateMixin` - * :class:`django.views.generic.detail.BaseDetailView` + * ``django.views.generic.detail.BaseDetailView`` * :class:`django.views.generic.detail.SingleObjectMixin` * :class:`django.views.generic.base.View` diff --git a/docs/ref/class-based-views/generic-display.txt b/docs/ref/class-based-views/generic-display.txt index 12603ff0df..b827c0005c 100644 --- a/docs/ref/class-based-views/generic-display.txt +++ b/docs/ref/class-based-views/generic-display.txt @@ -19,22 +19,22 @@ DetailView * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.detail.BaseDetailView` + * ``django.views.generic.detail.BaseDetailView`` * :class:`django.views.generic.detail.SingleObjectMixin` * :class:`django.views.generic.base.View` **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` - 3. :meth:`get_template_names()` - 4. :meth:`get_slug_field()` - 5. :meth:`get_queryset()` - 6. :meth:`get_object()` - 7. :meth:`get_context_object_name()` - 8. :meth:`get_context_data()` - 9. :meth:`get()` - 10. :meth:`render_to_response()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` + 3. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` + 4. :meth:`~django.views.generic.detail.SingleObjectMixin.get_slug_field()` + 5. :meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset()` + 6. :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` + 7. :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_object_name()` + 8. :meth:`~django.views.generic.detail.SingleObjectMixin.get_context_data()` + 9. ``get()`` + 10. :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` **Example views.py**:: @@ -86,14 +86,14 @@ ListView **Method Flowchart** - 1. :meth:`dispatch()` - 2. :meth:`http_method_not_allowed()` - 3. :meth:`get_template_names()` - 4. :meth:`get_queryset()` - 5. :meth:`get_objects()` - 6. :meth:`get_context_data()` - 7. :meth:`get()` - 8. :meth:`render_to_response()` + 1. :meth:`~django.views.generic.base.View.dispatch()` + 2. :meth:`~django.views.generic.base.View.http_method_not_allowed()` + 3. :meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` + 4. :meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset()` + 5. :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_object_name()` + 6. :meth:`~django.views.generic.list.MultipleObjectMixin.get_context_data()` + 7. ``get()`` + 8. :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` **Example views.py**:: @@ -140,7 +140,7 @@ ListView .. method:: get(request, *args, **kwargs) - Adds :attr:`object_list` to the context. If + Adds ``object_list`` to the context. If :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` is True then display an empty list. If :attr:`~django.views.generic.list.MultipleObjectMixin.allow_empty` is diff --git a/docs/ref/class-based-views/generic-editing.txt b/docs/ref/class-based-views/generic-editing.txt index 789dc2f84f..f3679287ad 100644 --- a/docs/ref/class-based-views/generic-editing.txt +++ b/docs/ref/class-based-views/generic-editing.txt @@ -38,7 +38,7 @@ FormView * :class:`django.views.generic.edit.FormView` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseFormView` + * ``django.views.generic.edit.BaseFormView`` * :class:`django.views.generic.edit.FormMixin` * :class:`django.views.generic.edit.ProcessFormView` * :class:`django.views.generic.base.View` @@ -86,7 +86,7 @@ CreateView * :class:`django.views.generic.edit.CreateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseCreateView` + * ``django.views.generic.edit.BaseCreateView`` * :class:`django.views.generic.edit.ModelFormMixin` * :class:`django.views.generic.edit.FormMixin` * :class:`django.views.generic.detail.SingleObjectMixin` @@ -128,7 +128,7 @@ UpdateView * :class:`django.views.generic.edit.UpdateView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseUpdateView` + * ``django.views.generic.edit.BaseUpdateView`` * :class:`django.views.generic.edit.ModelFormMixin` * :class:`django.views.generic.edit.FormMixin` * :class:`django.views.generic.detail.SingleObjectMixin` @@ -170,9 +170,9 @@ DeleteView * :class:`django.views.generic.edit.DeleteView` * :class:`django.views.generic.detail.SingleObjectTemplateResponseMixin` * :class:`django.views.generic.base.TemplateResponseMixin` - * :class:`django.views.generic.edit.BaseDeleteView` + * ``django.views.generic.edit.BaseDeleteView`` * :class:`django.views.generic.edit.DeletionMixin` - * :class:`django.views.generic.detail.BaseDetailView` + * ``django.views.generic.detail.BaseDetailView`` * :class:`django.views.generic.detail.SingleObjectMixin` * :class:`django.views.generic.base.View` diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 561e525e70..7ff201e5a2 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -100,7 +100,7 @@ MonthMixin :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. - .. method:: get_prev_month(date) + .. method:: get_previous_month(date) Returns a date object containing the first day of the month before the date provided. This function can also return ``None`` or raise an @@ -152,7 +152,7 @@ DayMixin :attr:`~BaseDateListView.allow_empty` and :attr:`~DateMixin.allow_future`. - .. method:: get_prev_day(date) + .. method:: get_previous_day(date) Returns a date object containing the previous valid day. This function can also return ``None`` or raise an :class:`~django.http.Http404` @@ -287,8 +287,9 @@ BaseDateListView available. If this is ``True`` and no objects are available, the view will display an empty page instead of raising a 404. - This is identical to :attr:`MultipleObjectMixin.allow_empty`, except - for the default value, which is ``False``. + This is identical to + :attr:`django.views.generic.list.MultipleObjectMixin.allow_empty`, + except for the default value, which is ``False``. .. attribute:: date_list_period diff --git a/docs/ref/class-based-views/mixins-editing.txt b/docs/ref/class-based-views/mixins-editing.txt index b8b59b827f..bce3c84cb1 100644 --- a/docs/ref/class-based-views/mixins-editing.txt +++ b/docs/ref/class-based-views/mixins-editing.txt @@ -83,9 +83,8 @@ FormMixin .. note:: - Views mixing :class:`FormMixin` must provide an implementation of - :meth:`~django.views.generic.FormMixin.form_valid` and - :meth:`~django.views.generic.FormMixin.form_invalid`. + Views mixing ``FormMixin`` must provide an implementation of + :meth:`form_valid` and :meth:`form_invalid`. ModelFormMixin @@ -93,15 +92,16 @@ ModelFormMixin .. class:: django.views.generic.edit.ModelFormMixin - A form mixin that works on ModelForms, rather than a standalone form. + A form mixin that works on ``ModelForms``, rather than a standalone form. Since this is a subclass of :class:`~django.views.generic.detail.SingleObjectMixin`, instances of this - mixin have access to the :attr:`~SingleObjectMixin.model` and - :attr:`~SingleObjectMixin.queryset` attributes, describing the type of - object that the ModelForm is manipulating. The view also provides - ``self.object``, the instance being manipulated. If the instance is being - created, ``self.object`` will be ``None``. + mixin have access to the + :attr:`~django.views.generic.detail.SingleObjectMixin.model` and + :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` attributes, + describing the type of object that the ``ModelForm`` is manipulating. The + view also provides ``self.object``, the instance being manipulated. If the + instance is being created, ``self.object`` will be ``None``. **Mixins** @@ -110,6 +110,12 @@ ModelFormMixin **Methods and Attributes** + .. attribute:: model + + A model class. Can be explicitly provided, otherwise will be determined + by examining ``self.object`` or + :attr:`~django.views.generic.detail.SingleObjectMixin.queryset`. + .. attribute:: success_url The URL to redirect to when the form is successfully processed. @@ -122,22 +128,25 @@ ModelFormMixin .. method:: get_form_class() Retrieve the form class to instantiate. If - :attr:`FormMixin.form_class` is provided, that class will be used. - Otherwise, a ModelForm will be instantiated using the model associated - with the :attr:`~SingleObjectMixin.queryset`, or with the - :attr:`~SingleObjectMixin.model`, depending on which attribute is - provided. + :attr:`~django.views.generic.edit.FormMixin.form_class` is provided, + that class will be used. Otherwise, a ``ModelForm`` will be + instantiated using the model associated with the + :attr:`~django.views.generic.detail.SingleObjectMixin.queryset`, or + with the :attr:`~django.views.generic.detail.SingleObjectMixin.model`, + depending on which attribute is provided. .. method:: get_form_kwargs() Add the current instance (``self.object``) to the standard - :meth:`FormMixin.get_form_kwargs`. + :meth:`~django.views.generic.edit.FormMixin.get_form_kwargs`. .. method:: get_success_url() Determine the URL to redirect to when the form is successfully - validated. Returns :attr:`ModelFormMixin.success_url` if it is provided; - otherwise, attempts to use the ``get_absolute_url()`` of the object. + validated. Returns + :attr:`django.views.generic.edit.ModelFormMixin.success_url` if it is + provided; otherwise, attempts to use the ``get_absolute_url()`` of the + object. .. method:: form_valid(form) diff --git a/docs/ref/class-based-views/mixins-multiple-object.txt b/docs/ref/class-based-views/mixins-multiple-object.txt index c85c962bce..b28bd11a71 100644 --- a/docs/ref/class-based-views/mixins-multiple-object.txt +++ b/docs/ref/class-based-views/mixins-multiple-object.txt @@ -61,14 +61,13 @@ MultipleObjectMixin .. attribute:: queryset A ``QuerySet`` that represents the objects. If provided, the value of - :attr:`MultipleObjectMixin.queryset` supersedes the value provided for - :attr:`MultipleObjectMixin.model`. + ``queryset`` supersedes the value provided for :attr:`model`. .. attribute:: paginate_by An integer specifying how many objects should be displayed per page. If this is given, the view will paginate objects with - :attr:`MultipleObjectMixin.paginate_by` objects per page. The view will + ``paginate_by`` objects per page. The view will expect either a ``page`` query string parameter (via ``request.GET``) or a ``page`` variable specified in the URLconf. @@ -77,10 +76,9 @@ MultipleObjectMixin .. versionadded:: 1.6 An integer specifying the number of "overflow" objects the last page - can contain. This extends the :attr:`MultipleObjectMixin.paginate_by` - limit on the last page by up to - :attr:`MultipleObjectMixin.paginate_orphans`, in order to keep the last - page from having a very small number of objects. + can contain. This extends the :attr:`paginate_by` limit on the last + page by up to ``paginate_orphans``, in order to keep the last page from + having a very small number of objects. .. attribute:: page_kwarg @@ -97,7 +95,7 @@ MultipleObjectMixin :class:`django.core.paginator.Paginator` is used. If the custom paginator class doesn't have the same constructor interface as :class:`django.core.paginator.Paginator`, you will also need to - provide an implementation for :meth:`MultipleObjectMixin.get_paginator`. + provide an implementation for :meth:`get_paginator`. .. attribute:: context_object_name @@ -122,20 +120,20 @@ MultipleObjectMixin Returns the number of items to paginate by, or ``None`` for no pagination. By default this simply returns the value of - :attr:`MultipleObjectMixin.paginate_by`. + :attr:`paginate_by`. .. method:: get_paginator(queryset, per_page, orphans=0, allow_empty_first_page=True) Returns an instance of the paginator to use for this view. By default, instantiates an instance of :attr:`paginator_class`. - .. method:: get_paginate_by() + .. method:: get_paginate_orphans() .. versionadded:: 1.6 An integer specifying the number of "overflow" objects the last page can contain. By default this simply returns the value of - :attr:`MultipleObjectMixin.paginate_orphans`. + :attr:`paginate_orphans`. .. method:: get_allow_empty() @@ -149,7 +147,7 @@ MultipleObjectMixin Return the context variable name that will be used to contain the list of data that this view is manipulating. If ``object_list`` is a queryset of Django objects and - :attr:`~MultipleObjectMixin.context_object_name` is not set, + :attr:`context_object_name` is not set, the context name will be the ``object_name`` of the model that the queryset is composed from, with postfix ``'_list'`` appended. For example, the model ``Article`` would have a diff --git a/docs/ref/class-based-views/mixins-simple.txt b/docs/ref/class-based-views/mixins-simple.txt index d2f0df241e..e2e6084e8e 100644 --- a/docs/ref/class-based-views/mixins-simple.txt +++ b/docs/ref/class-based-views/mixins-simple.txt @@ -48,7 +48,7 @@ TemplateResponseMixin .. attribute:: template_name The full name of a template to use as defined by a string. Not defining - a template_name will raise a + a ``template_name`` will raise a :class:`django.core.exceptions.ImproperlyConfigured` exception. .. attribute:: response_class @@ -73,15 +73,13 @@ TemplateResponseMixin If any keyword arguments are provided, they will be passed to the constructor of the response class. - Calls :meth:`~TemplateResponseMixin.get_template_names()` to obtain the - list of template names that will be searched looking for an existent - template. + Calls :meth:`get_template_names()` to obtain the list of template names + that will be searched looking for an existent template. .. method:: get_template_names() Returns a list of template names to search for when rendering the template. - If :attr:`TemplateResponseMixin.template_name` is specified, the - default implementation will return a list containing - :attr:`TemplateResponseMixin.template_name` (if it is specified). + If :attr:`template_name` is specified, the default implementation will + return a list containing :attr:`template_name` (if it is specified). diff --git a/docs/ref/class-based-views/mixins-single-object.txt b/docs/ref/class-based-views/mixins-single-object.txt index e84ba6b8dd..299ac56ac6 100644 --- a/docs/ref/class-based-views/mixins-single-object.txt +++ b/docs/ref/class-based-views/mixins-single-object.txt @@ -21,8 +21,7 @@ SingleObjectMixin .. attribute:: queryset A ``QuerySet`` that represents the objects. If provided, the value of - :attr:`SingleObjectMixin.queryset` supersedes the value provided for - :attr:`SingleObjectMixin.model`. + ``queryset`` supersedes the value provided for :attr:`model`. .. attribute:: slug_field @@ -47,38 +46,38 @@ SingleObjectMixin Returns the single object that this view will display. If ``queryset`` is provided, that queryset will be used as the - source of objects; otherwise, - :meth:`~SingleObjectMixin.get_queryset` will be used. - ``get_object()`` looks for a - :attr:`SingleObjectMixin.pk_url_kwarg` argument in the arguments - to the view; if this argument is found, this method performs a - primary-key based lookup using that value. If this argument is not - found, it looks for a :attr:`SingleObjectMixin.slug_url_kwarg` - argument, and performs a slug lookup using the - :attr:`SingleObjectMixin.slug_field`. + source of objects; otherwise, :meth:`get_queryset` will be used. + ``get_object()`` looks for a :attr:`pk_url_kwarg` argument in the + arguments to the view; if this argument is found, this method performs + a primary-key based lookup using that value. If this argument is not + found, it looks for a :attr:`slug_url_kwarg` argument, and performs a + slug lookup using the :attr:`slug_field`. .. method:: get_queryset() Returns the queryset that will be used to retrieve the object that - this view will display. By default, - :meth:`~SingleObjectMixin.get_queryset` returns the value of the - :attr:`~SingleObjectMixin.queryset` attribute if it is set, otherwise - it constructs a :class:`QuerySet` by calling the `all()` method on the - :attr:`~SingleObjectMixin.model` attribute's default manager. + this view will display. By default, :meth:`get_queryset` returns the + value of the :attr:`queryset` attribute if it is set, otherwise + it constructs a :class:`~django.db.models.query.QuerySet` by calling + the `all()` method on the :attr:`model` attribute's default manager. .. method:: get_context_object_name(obj) Return the context variable name that will be used to contain the - data that this view is manipulating. If - :attr:`~SingleObjectMixin.context_object_name` is not set, the context - name will be constructed from the ``object_name`` of the model that - the queryset is composed from. For example, the model ``Article`` - would have context object named ``'article'``. + data that this view is manipulating. If :attr:`context_object_name` is + not set, the context name will be constructed from the ``object_name`` + of the model that the queryset is composed from. For example, the model + ``Article`` would have context object named ``'article'``. .. method:: get_context_data(**kwargs) Returns context data for displaying the list of objects. + .. method:: get_slug_field() + + Returns the name of a slug field to be used to look up by slug. By + default this simply returns the value of :attr:`slug_field`. + **Context** * ``object``: The object that this view is displaying. If diff --git a/docs/ref/clickjacking.txt b/docs/ref/clickjacking.txt index 15e85b43b7..e3d1bfc87b 100644 --- a/docs/ref/clickjacking.txt +++ b/docs/ref/clickjacking.txt @@ -111,10 +111,10 @@ Browsers that support X-Frame-Options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * Internet Explorer 8+ -* Firefox 3.6.9+ -* Opera 10.5+ -* Safari 4+ -* Chrome 4.1+ +* Firefox 3.6.9+ +* Opera 10.5+ +* Safari 4+ +* Chrome 4.1+ See also ~~~~~~~~ diff --git a/docs/ref/contrib/admin/admindocs.txt b/docs/ref/contrib/admin/admindocs.txt index 4a50856f3d..b3e26eca48 100644 --- a/docs/ref/contrib/admin/admindocs.txt +++ b/docs/ref/contrib/admin/admindocs.txt @@ -24,7 +24,7 @@ the following: * Add :mod:`django.contrib.admindocs` to your :setting:`INSTALLED_APPS`. * Add ``(r'^admin/doc/', include('django.contrib.admindocs.urls'))`` to - your :data:`urlpatterns`. Make sure it's included *before* the + your ``urlpatterns``. Make sure it's included *before* the ``r'^admin/'`` entry, so that requests to ``/admin/doc/`` don't get handled by the latter entry. * Install the docutils Python module (http://docutils.sf.net/). diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index ec63cb2dcc..04a7824417 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -170,7 +170,7 @@ subclass:: ``fields`` option (for more complex layout needs see the :attr:`~ModelAdmin.fieldsets` option described in the next section). For example, you could define a simpler version of the admin form for the - ``django.contrib.flatpages.FlatPage`` model as follows:: + :class:`django.contrib.flatpages.models.FlatPage` model as follows:: class FlatPageAdmin(admin.ModelAdmin): fields = ('url', 'title', 'content') @@ -212,8 +212,8 @@ subclass:: a dictionary of information about the fieldset, including a list of fields to be displayed in it. - A full example, taken from the :class:`django.contrib.flatpages.FlatPage` - model:: + A full example, taken from the + :class:`django.contrib.flatpages.models.FlatPage` model:: class FlatPageAdmin(admin.ModelAdmin): fieldsets = ( @@ -357,7 +357,7 @@ subclass:: Note that the key in the dictionary is the actual field class, *not* a string. The value is another dictionary; these arguments will be passed to - :meth:`~django.forms.Field.__init__`. See :doc:`/ref/forms/api` for + the form field's ``__init__()`` method. See :doc:`/ref/forms/api` for details. .. warning:: @@ -584,7 +584,7 @@ subclass:: class PersonAdmin(UserAdmin): list_filter = ('company__name',) - * a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`, + * a class inheriting from ``django.contrib.admin.SimpleListFilter``, which you need to provide the ``title`` and ``parameter_name`` attributes to and override the ``lookups`` and ``queryset`` methods, e.g.:: @@ -671,7 +671,7 @@ subclass:: * a tuple, where the first element is a field name and the second element is a class inheriting from - :mod:`django.contrib.admin.FieldListFilter`, for example:: + ``django.contrib.admin.FieldListFilter``, for example:: from django.contrib.admin import BooleanFieldListFilter @@ -943,10 +943,9 @@ templates used by the :class:`ModelAdmin` views: .. attribute:: ModelAdmin.delete_selected_confirmation_template - Path to a custom template, used by the :meth:`delete_selected` - action method for displaying a confirmation page when deleting one - or more objects. See the :doc:`actions - documentation`. + Path to a custom template, used by the ``delete_selected`` action method + for displaying a confirmation page when deleting one or more objects. See + the :doc:`actions documentation`. .. attribute:: ModelAdmin.object_history_template @@ -1108,9 +1107,8 @@ templates used by the :class:`ModelAdmin` views: Since this is usually not what you want, Django provides a convenience wrapper to check permissions and mark the view as non-cacheable. This - wrapper is :meth:`AdminSite.admin_view` (i.e. - ``self.admin_site.admin_view`` inside a ``ModelAdmin`` instance); use it - like so:: + wrapper is ``AdminSite.admin_view()`` (i.e. ``self.admin_site.admin_view`` + inside a ``ModelAdmin`` instance); use it like so:: class MyModelAdmin(admin.ModelAdmin): def get_urls(self): @@ -1130,7 +1128,7 @@ templates used by the :class:`ModelAdmin` views: If the page is cacheable, but you still want the permission check to be performed, you can pass a ``cacheable=True`` argument to - :meth:`AdminSite.admin_view`:: + ``AdminSite.admin_view()``:: (r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True)) diff --git a/docs/ref/contrib/comments/custom.txt b/docs/ref/contrib/comments/custom.txt index 0ef37a9a0b..b4ab65bc2d 100644 --- a/docs/ref/contrib/comments/custom.txt +++ b/docs/ref/contrib/comments/custom.txt @@ -66,15 +66,17 @@ In the ``models.py`` we'll define a ``CommentWithTitle`` model:: class CommentWithTitle(Comment): title = models.CharField(max_length=300) -Most custom comment models will subclass the :class:`Comment` model. However, +Most custom comment models will subclass the +:class:`~django.contrib.comments.models.Comment` model. However, if you want to substantially remove or change the fields available in the -:class:`Comment` model, but don't want to rewrite the templates, you could -try subclassing from :class:`BaseCommentAbstractModel`. +:class:`~django.contrib.comments.models.Comment` model, but don't want to +rewrite the templates, you could try subclassing from +``BaseCommentAbstractModel``. Next, we'll define a custom comment form in ``forms.py``. This is a little more tricky: we have to both create a form and override -:meth:`CommentForm.get_comment_model` and -:meth:`CommentForm.get_comment_create_data` to return deal with our custom title +``CommentForm.get_comment_model()`` and +``CommentForm.get_comment_create_data()`` to return deal with our custom title field:: from django import forms @@ -139,7 +141,7 @@ however. Return the :class:`~django.db.models.Model` class to use for comments. This model should inherit from - :class:`django.contrib.comments.models.BaseCommentAbstractModel`, which + ``django.contrib.comments.models.BaseCommentAbstractModel``, which defines necessary core fields. The default implementation returns @@ -170,33 +172,33 @@ however. attribute when rendering your comment form. The default implementation returns a reverse-resolved URL pointing - to the :func:`post_comment` view. + to the ``post_comment()`` view. .. note:: If you provide a custom comment model and/or form, but you - want to use the default :func:`post_comment` view, you will + want to use the default ``post_comment()`` view, you will need to be aware that it requires the model and form to have certain additional attributes and methods: see the - :func:`post_comment` view documentation for details. + ``django.contrib.comments.views.post_comment()`` view for details. .. function:: get_flag_url() Return the URL for the "flag this comment" view. The default implementation returns a reverse-resolved URL pointing - to the :func:`django.contrib.comments.views.moderation.flag` view. + to the ``django.contrib.comments.views.moderation.flag()`` view. .. function:: get_delete_url() Return the URL for the "delete this comment" view. The default implementation returns a reverse-resolved URL pointing - to the :func:`django.contrib.comments.views.moderation.delete` view. + to the ``django.contrib.comments.views.moderation.delete()`` view. .. function:: get_approve_url() Return the URL for the "approve this comment from moderation" view. The default implementation returns a reverse-resolved URL pointing - to the :func:`django.contrib.comments.views.moderation.approve` view. + to the ``django.contrib.comments.views.moderation.approve()`` view. diff --git a/docs/ref/contrib/comments/example.txt b/docs/ref/contrib/comments/example.txt index 2bff778c2f..4e18e37de0 100644 --- a/docs/ref/contrib/comments/example.txt +++ b/docs/ref/contrib/comments/example.txt @@ -136,7 +136,7 @@ Feeds ===== Suppose you want to export a :doc:`feed ` of the -latest comments, you can use the built-in :class:`LatestCommentFeed`. Just +latest comments, you can use the built-in ``LatestCommentFeed``. Just enable it in your project's ``urls.py``: .. code-block:: python @@ -166,7 +166,7 @@ features (all of which or only certain can be enabled): * Close comments after a particular (user-defined) number of days. * Email new comments to the site-staff. -To enable comment moderation, we subclass the :class:`CommentModerator` and +To enable comment moderation, we subclass the ``CommentModerator`` and register it with the moderation features we want. Let's suppose we want to close comments after 7 days of posting and also send out an email to the site staff. In ``blog/models.py``, we register a comment moderator in the diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index c042971d39..a7138dda53 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -185,15 +185,14 @@ via two methods: be moderated using the options defined in the ``CommentModerator`` subclass. If any of the models are already registered for moderation, the exception - :exc:`AlreadyModerated` will be raised. + ``AlreadyModerated`` will be raised. .. function:: moderator.unregister(model_or_iterable) Takes one argument: a model class or list of model classes, and removes the model or models from the set of models which are being moderated. If any of the models are not currently - being moderated, the exception - :exc:`NotModerated` will be raised. + being moderated, the exception ``NotModerated`` will be raised. Customizing the moderation system @@ -207,8 +206,8 @@ models with an instance of the subclass. .. class:: Moderator - In addition to the :meth:`Moderator.register` and - :meth:`Moderator.unregister` methods detailed above, the following methods + In addition to the :func:`moderator.register` and + :func:`moderator.unregister` methods detailed above, the following methods on :class:`Moderator` can be overridden to achieve customized behavior: .. method:: connect diff --git a/docs/ref/contrib/comments/signals.txt b/docs/ref/contrib/comments/signals.txt index 8274539ed7..ea901b6a95 100644 --- a/docs/ref/contrib/comments/signals.txt +++ b/docs/ref/contrib/comments/signals.txt @@ -81,8 +81,8 @@ Arguments sent with this signal: :meth:`~django.db.models.Model.save` again. ``flag`` - The :class:`~django.contrib.comments.models.CommentFlag` that's been - attached to the comment. + The ``django.contrib.comments.models.CommentFlag`` that's been attached to + the comment. ``created`` ``True`` if this is a new flag; ``False`` if it's a duplicate flag. diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 8f329aa388..e9cd5e7bc0 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -453,7 +453,7 @@ Generic relations in forms and admin ------------------------------------ The :mod:`django.contrib.contenttypes.generic` module provides -:class:`~django.contrib.contenttypes.generic.BaseGenericInlineFormSet`, +``BaseGenericInlineFormSet``, :class:`~django.contrib.contenttypes.generic.GenericTabularInline` and :class:`~django.contrib.contenttypes.generic.GenericStackedInline` (the last two are subclasses of @@ -480,3 +480,9 @@ information. The name of the integer field that represents the ID of the related object. Defaults to ``object_id``. + +.. class:: GenericTabularInline +.. class:: GenericStackedInline + + Subclasses of :class:`GenericInlineModelAdmin` with stacked and tabular + layouts, respectively. diff --git a/docs/ref/contrib/flatpages.txt b/docs/ref/contrib/flatpages.txt index c360809dac..292b304acb 100644 --- a/docs/ref/contrib/flatpages.txt +++ b/docs/ref/contrib/flatpages.txt @@ -186,7 +186,7 @@ Via the Python API If you add or modify flatpages via your own code, you will likely want to check for duplicate flatpage URLs within the same site. The flatpage form used in the admin performs this validation check, and can be imported from - :class:`django.contrib.flatpages.forms.FlatPageForm` and used in your own + ``django.contrib.flatpages.forms.FlatPageForm`` and used in your own views. Flatpage templates @@ -256,7 +256,7 @@ Displaying ``registration_required`` flatpages By default, the :ttag:`get_flatpages` templatetag will only show flatpages that are marked ``registration_required = False``. If you want to display registration-protected flatpages, you need to specify -an authenticated user using a``for`` clause. +an authenticated user using a ``for`` clause. For example: diff --git a/docs/ref/contrib/formtools/form-preview.txt b/docs/ref/contrib/formtools/form-preview.txt index 784213ecba..011e72c2e0 100644 --- a/docs/ref/contrib/formtools/form-preview.txt +++ b/docs/ref/contrib/formtools/form-preview.txt @@ -25,9 +25,8 @@ application takes care of the following workflow: a. If it's valid, displays a preview page. b. If it's not valid, redisplays the form with error messages. 3. When the "confirmation" form is submitted from the preview page, calls - a hook that you define -- a - :meth:`~django.contrib.formtools.preview.FormPreview.done()` method that gets - passed the valid data. + a hook that you define -- a ``done()`` method that gets passed the valid + data. The framework enforces the required preview by passing a shared-secret hash to the preview page via hidden form fields. If somebody tweaks the form parameters @@ -51,8 +50,7 @@ How to use ``FormPreview`` directory to your :setting:`TEMPLATE_DIRS` setting. 2. Create a :class:`~django.contrib.formtools.preview.FormPreview` subclass that - overrides the :meth:`~django.contrib.formtools.preview.FormPreview.done()` - method:: + overrides the ``done()`` method:: from django.contrib.formtools.preview import FormPreview from myapp.models import SomeModel @@ -92,13 +90,15 @@ How to use ``FormPreview`` A :class:`~django.contrib.formtools.preview.FormPreview` class is a simple Python class that represents the preview workflow. :class:`~django.contrib.formtools.preview.FormPreview` classes must subclass -``django.contrib.formtools.preview.FormPreview`` and override the -:meth:`~django.contrib.formtools.preview.FormPreview.done()` method. They can live -anywhere in your codebase. +``django.contrib.formtools.preview.FormPreview`` and override the ``done()`` +method. They can live anywhere in your codebase. ``FormPreview`` templates ========================= +.. attribute:: FormPreview.form_template +.. attribute:: FormPreview.preview_template + By default, the form is rendered via the template :file:`formtools/form.html`, and the preview page is rendered via the template :file:`formtools/preview.html`. These values can be overridden for a particular form preview by setting diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index 3edc019d05..9ea65d7e5f 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -54,7 +54,8 @@ you just have to do these things: 4. Add ``django.contrib.formtools`` to your :setting:`INSTALLED_APPS` list in your settings file. -5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` method. +5. Point your URLconf at your :class:`WizardView` :meth:`~WizardView.as_view` + method. Defining ``Form`` classes ------------------------- @@ -89,6 +90,9 @@ the message itself. Here's what the :file:`forms.py` might look like:: Creating a ``WizardView`` subclass ---------------------------------- +.. class:: SessionWizardView +.. class:: CookieWizardView + The next step is to create a :class:`django.contrib.formtools.wizard.views.WizardView` subclass. You can also use the :class:`SessionWizardView` or :class:`CookieWizardView` classes @@ -225,9 +229,11 @@ Here's a full example template: Hooking the wizard into a URLconf --------------------------------- +.. method:: WizardView.as_view + Finally, we need to specify which forms to use in the wizard, and then deploy the new :class:`WizardView` object at a URL in the ``urls.py``. The -wizard's :meth:`as_view` method takes a list of your +wizard's ``as_view()`` method takes a list of your :class:`~django.forms.Form` classes as an argument during instantiation:: from django.conf.urls import patterns @@ -346,9 +352,9 @@ Advanced ``WizardView`` methods used as the form for step ``step``. Returns an :class:`~django.db.models.Model` object which will be passed as - the :attr:`~django.forms.ModelForm.instance` argument when instantiating the - ModelForm for step ``step``. If no instance object was provided while - initializing the form wizard, ``None`` will be returned. + the ``instance`` argument when instantiating the ``ModelForm`` for step + ``step``. If no instance object was provided while initializing the form + wizard, ``None`` will be returned. The default implementation:: @@ -514,10 +520,10 @@ Providing initial data for the forms .. attribute:: WizardView.initial_dict Initial data for a wizard's :class:`~django.forms.Form` objects can be - provided using the optional :attr:`~Wizard.initial_dict` keyword argument. - This argument should be a dictionary mapping the steps to dictionaries - containing the initial data for each step. The dictionary of initial data - will be passed along to the constructor of the step's + provided using the optional :attr:`~WizardView.initial_dict` keyword + argument. This argument should be a dictionary mapping the steps to + dictionaries containing the initial data for each step. The dictionary of + initial data will be passed along to the constructor of the step's :class:`~django.forms.Form`:: >>> from myapp.forms import ContactForm1, ContactForm2 @@ -542,11 +548,13 @@ Providing initial data for the forms Handling files ============== +.. attribute:: WizardView.file_storage + To handle :class:`~django.forms.FileField` within any step form of the wizard, -you have to add a :attr:`file_storage` to your :class:`WizardView` subclass. +you have to add a ``file_storage`` to your :class:`WizardView` subclass. This storage will temporarily store the uploaded files for the wizard. The -:attr:`file_storage` attribute should be a +``file_storage`` attribute should be a :class:`~django.core.files.storage.Storage` subclass. Django provides a built-in storage class (see :ref:`the built-in filesystem @@ -646,6 +654,8 @@ Usage of ``NamedUrlWizardView`` =============================== .. class:: NamedUrlWizardView +.. class:: NamedUrlSessionWizardView +.. class:: NamedUrlCookieWizardView There is a :class:`WizardView` subclass which adds named-urls support to the wizard. By doing this, you can have single urls for every step. You can also diff --git a/docs/ref/contrib/formtools/index.txt b/docs/ref/contrib/formtools/index.txt index f36470654a..e768c0e655 100644 --- a/docs/ref/contrib/formtools/index.txt +++ b/docs/ref/contrib/formtools/index.txt @@ -1,6 +1,8 @@ django.contrib.formtools ======================== +.. module:: django.contrib.formtools + A set of high-level abstractions for Django forms (:mod:`django.forms`). .. toctree:: diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt index 519f79f0d4..be413c9df8 100644 --- a/docs/ref/contrib/gis/db-api.txt +++ b/docs/ref/contrib/gis/db-api.txt @@ -4,20 +4,23 @@ GeoDjango Database API ====================== -.. module:: django.contrib.gis.db.models - :synopsis: GeoDjango's database API. - .. _spatial-backends: Spatial Backends ================ +.. module:: django.contrib.gis.db.backends + :synopsis: GeoDjango's spatial database backends. + GeoDjango currently provides the following spatial database backends: -* :mod:`django.contrib.gis.db.backends.postgis` -* :mod:`django.contrib.gis.db.backends.mysql` -* :mod:`django.contrib.gis.db.backends.oracle` -* :mod:`django.contrib.gis.db.backends.spatialite` +* ``django.contrib.gis.db.backends.postgis`` +* ``django.contrib.gis.db.backends.mysql`` +* ``django.contrib.gis.db.backends.oracle`` +* ``django.contrib.gis.db.backends.spatialite`` + +.. module:: django.contrib.gis.db.models + :synopsis: GeoDjango's database API. .. _mysql-spatial-limitations: diff --git a/docs/ref/contrib/gis/feeds.txt b/docs/ref/contrib/gis/feeds.txt index 7c3a2d011c..7b1b6ebccf 100644 --- a/docs/ref/contrib/gis/feeds.txt +++ b/docs/ref/contrib/gis/feeds.txt @@ -27,7 +27,7 @@ API Reference .. class:: Feed In addition to methods provided by - the :class:`django.contrib.syndication.feeds.Feed` + the :class:`django.contrib.syndication.views.Feed` base class, GeoDjango's ``Feed`` class provides the following overrides. Note that these overrides may be done in multiple ways:: @@ -71,11 +71,11 @@ API Reference can be a ``GEOSGeometry`` instance, or a tuple that represents a point coordinate or bounding box. For example:: - class ZipcodeFeed(Feed): + class ZipcodeFeed(Feed): - def item_geometry(self, obj): - # Returns the polygon. - return obj.poly + def item_geometry(self, obj): + # Returns the polygon. + return obj.poly ``SyndicationFeed`` Subclasses ------------------------------ diff --git a/docs/ref/contrib/gis/geoquerysets.txt b/docs/ref/contrib/gis/geoquerysets.txt index 69280dc028..66afc3d377 100644 --- a/docs/ref/contrib/gis/geoquerysets.txt +++ b/docs/ref/contrib/gis/geoquerysets.txt @@ -683,7 +683,7 @@ Keyword Argument Description a method name clashes with an existing ``GeoQuerySet`` method -- if you wanted to use the ``area()`` method on model with a ``PolygonField`` - named ``area``, for example. + named ``area``, for example. ===================== ===================================================== Measurement @@ -1043,7 +1043,7 @@ Keyword Argument Description ===================== ===================================================== ``relative`` If set to ``True``, the path data will be implemented in terms of relative moves. Defaults to ``False``, - meaning that absolute moves are used instead. + meaning that absolute moves are used instead. ``precision`` This keyword may be used to specify the number of significant digits for the coordinates in the SVG diff --git a/docs/ref/contrib/gis/geos.txt b/docs/ref/contrib/gis/geos.txt index 7d7c32781c..4d44638488 100644 --- a/docs/ref/contrib/gis/geos.txt +++ b/docs/ref/contrib/gis/geos.txt @@ -142,10 +142,9 @@ Geometry Objects .. class:: GEOSGeometry(geo_input[, srid=None]) - :param geo_input: Geometry input value - :type geo_input: string or buffer + :param geo_input: Geometry input value (string or buffer) :param srid: spatial reference identifier - :type srid: integer + :type srid: int This is the base class for all GEOS geometry objects. It initializes on the given ``geo_input`` argument, and then assumes the proper geometry subclass @@ -800,7 +799,7 @@ Example:: :param string: string that contains spatial data :type string: string :param srid: spatial reference identifier - :type srid: integer + :type srid: int :rtype: a :class:`GEOSGeometry` corresponding to the spatial data in the string Example:: @@ -966,3 +965,10 @@ location (e.g., ``/home/bob/lib/libgeos_c.so``). The setting must be the *full* path to the **C** shared library; in other words you want to use ``libgeos_c.so``, not ``libgeos.so``. + +Exceptions +========== + +.. exception:: GEOSException + +The base GEOS exception, indicates a GEOS-related error. diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index 100dc2edd0..35c01c9b7e 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -530,6 +530,6 @@ Finally, :ref:`install Django ` on your system. .. rubric:: Footnotes .. [#] GeoDjango uses the :func:`~ctypes.util.find_library` routine from - :mod:`ctypes.util` to locate shared libraries. + ``ctypes.util`` to locate shared libraries. .. [#] The ``psycopg2`` Windows installers are packaged and maintained by `Jason Erickson `_. diff --git a/docs/ref/contrib/gis/tutorial.txt b/docs/ref/contrib/gis/tutorial.txt index 5000622ad4..9efa020e61 100644 --- a/docs/ref/contrib/gis/tutorial.txt +++ b/docs/ref/contrib/gis/tutorial.txt @@ -226,7 +226,7 @@ model to represent this data:: class WorldBorder(models.Model): # Regular Django fields corresponding to the attributes in the - # world borders shapefile. + # world borders shapefile. name = models.CharField(max_length=50) area = models.IntegerField() pop2005 = models.IntegerField('Population 2005') @@ -236,13 +236,13 @@ model to represent this data:: un = models.IntegerField('United Nations Code') region = models.IntegerField('Region Code') subregion = models.IntegerField('Sub-Region Code') - lon = models.FloatField() - lat = models.FloatField() + lon = models.FloatField() + lat = models.FloatField() - # GeoDjango-specific: a geometry field (MultiPolygonField), and + # GeoDjango-specific: a geometry field (MultiPolygonField), and # overriding the default manager with a GeoManager instance. - mpoly = models.MultiPolygonField() - objects = models.GeoManager() + mpoly = models.MultiPolygonField() + objects = models.GeoManager() # Returns the string representation of the model. def __unicode__(self): @@ -250,7 +250,7 @@ model to represent this data:: Please note two important things: -1. The ``models`` module is imported from :mod:`django.contrib.gis.db`. +1. The ``models`` module is imported from ``django.contrib.gis.db``. 2. You must override the model's default manager with :class:`~django.contrib.gis.db.models.GeoManager` to perform spatial queries. diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index 42c4b91bd4..1861318b95 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -49,6 +49,8 @@ loader can find the default templates.) Initialization ============== +.. function:: views.sitemap(request, sitemaps, section=None, template_name='sitemap.xml', mimetype='application/xml') + To activate sitemap generation on your Django site, add this line to your :doc:`URLconf `:: @@ -240,9 +242,9 @@ The sitemap framework provides a couple convenience classes for common cases: The :class:`django.contrib.sitemaps.GenericSitemap` class allows you to create a sitemap by passing it a dictionary which has to contain at least - a :data:`queryset` entry. This queryset will be used to generate the items - of the sitemap. It may also have a :data:`date_field` entry that - specifies a date field for objects retrieved from the :data:`queryset`. + a ``queryset`` entry. This queryset will be used to generate the items + of the sitemap. It may also have a ``date_field`` entry that + specifies a date field for objects retrieved from the ``queryset``. This will be used for the :attr:`~Sitemap.lastmod` attribute in the generated sitemap. You may also pass :attr:`~Sitemap.priority` and :attr:`~Sitemap.changefreq` keyword arguments to the @@ -281,14 +283,16 @@ Here's an example of a :doc:`URLconf ` using both:: Creating a sitemap index ======================== +.. function:: views.index(request, sitemaps, template_name='sitemap_index.xml', mimetype='application/xml', sitemap_url_name='django.contrib.sitemaps.views.sitemap') + The sitemap framework also has the ability to create a sitemap index that references individual sitemap files, one per each section defined in your -:data:`sitemaps` dictionary. The only differences in usage are: +``sitemaps`` dictionary. The only differences in usage are: * You use two views in your URLconf: :func:`django.contrib.sitemaps.views.index` and :func:`django.contrib.sitemaps.views.sitemap`. * The :func:`django.contrib.sitemaps.views.sitemap` view should take a - :data:`section` keyword argument. + ``section`` keyword argument. Here's what the relevant URLconf lines would look like for the example above:: @@ -299,7 +303,7 @@ Here's what the relevant URLconf lines would look like for the example above:: This will automatically generate a :file:`sitemap.xml` file that references both :file:`sitemap-flatpages.xml` and :file:`sitemap-blog.xml`. The -:class:`~django.contrib.sitemaps.Sitemap` classes and the :data:`sitemaps` +:class:`~django.contrib.sitemaps.Sitemap` classes and the ``sitemaps`` dict don't change at all. You should create an index file if one of your sitemaps has more than 50,000 @@ -350,19 +354,20 @@ rendering. For more details, see the :doc:`TemplateResponse documentation Context variables ------------------ -When customizing the templates for the :func:`~django.contrib.sitemaps.views.index` -and :func:`~django.contrib.sitemaps.views.sitemaps` views, you can rely on the +When customizing the templates for the +:func:`~django.contrib.sitemaps.views.index` and +:func:`~django.contrib.sitemaps.views.sitemap` views, you can rely on the following context variables. Index ----- -The variable :data:`sitemaps` is a list of absolute URLs to each of the sitemaps. +The variable ``sitemaps`` is a list of absolute URLs to each of the sitemaps. Sitemap ------- -The variable :data:`urlset` is a list of URLs that should appear in the +The variable ``urlset`` is a list of URLs that should appear in the sitemap. Each URL exposes attributes as defined in the :class:`~django.contrib.sitemaps.Sitemap` class: @@ -411,14 +416,14 @@ that: :func:`django.contrib.sitemaps.ping_google()`. .. function:: ping_google - :func:`ping_google` takes an optional argument, :data:`sitemap_url`, + :func:`ping_google` takes an optional argument, ``sitemap_url``, which should be the absolute path to your site's sitemap (e.g., :file:`'/sitemap.xml'`). If this argument isn't provided, :func:`ping_google` will attempt to figure out your sitemap by performing a reverse looking in your URLconf. :func:`ping_google` raises the exception - :exc:`django.contrib.sitemaps.SitemapNotFound` if it cannot determine your + ``django.contrib.sitemaps.SitemapNotFound`` if it cannot determine your sitemap URL. .. admonition:: Register with Google first! diff --git a/docs/ref/contrib/staticfiles.txt b/docs/ref/contrib/staticfiles.txt index 9c8f29a8de..a4a60f239b 100644 --- a/docs/ref/contrib/staticfiles.txt +++ b/docs/ref/contrib/staticfiles.txt @@ -33,7 +33,7 @@ STATICFILES_DIRS Default: ``[]`` This setting defines the additional locations the staticfiles app will traverse -if the :class:`FileSystemFinder` finder is enabled, e.g. if you use the +if the ``FileSystemFinder`` finder is enabled, e.g. if you use the :djadmin:`collectstatic` or :djadmin:`findstatic` management command or use the static file serving view. @@ -101,19 +101,19 @@ The list of finder backends that know how to find static files in various locations. The default will find files stored in the :setting:`STATICFILES_DIRS` setting -(using :class:`django.contrib.staticfiles.finders.FileSystemFinder`) and in a +(using ``django.contrib.staticfiles.finders.FileSystemFinder``) and in a ``static`` subdirectory of each app (using -:class:`django.contrib.staticfiles.finders.AppDirectoriesFinder`) +``django.contrib.staticfiles.finders.AppDirectoriesFinder``) One finder is disabled by default: -:class:`django.contrib.staticfiles.finders.DefaultStorageFinder`. If added to +``django.contrib.staticfiles.finders.DefaultStorageFinder``. If added to your :setting:`STATICFILES_FINDERS` setting, it will look for static files in the default file storage as defined by the :setting:`DEFAULT_FILE_STORAGE` setting. .. note:: - When using the :class:`AppDirectoriesFinder` finder, make sure your apps + When using the ``AppDirectoriesFinder`` finder, make sure your apps can be found by staticfiles. Simply add the app to the :setting:`INSTALLED_APPS` setting of your site. diff --git a/docs/ref/contrib/syndication.txt b/docs/ref/contrib/syndication.txt index 2418dba8ef..d0376e3c1b 100644 --- a/docs/ref/contrib/syndication.txt +++ b/docs/ref/contrib/syndication.txt @@ -334,7 +334,7 @@ And the accompanying URLconf:: Feed class reference -------------------- -.. class:: django.contrib.syndication.views.Feed +.. class:: views.Feed This example illustrates all possible attributes and methods for a :class:`~django.contrib.syndication.views.Feed` class:: diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index 771085766e..e933ee350d 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -259,9 +259,9 @@ recommended solution. Should you decide to use ``utf8_bin`` collation for some of your tables with MySQLdb 1.2.1p2 or 1.2.2, you should still use ``utf8_collation_ci_swedish`` -(the default) collation for the :class:`django.contrib.sessions.models.Session` +(the default) collation for the ``django.contrib.sessions.models.Session`` table (usually called ``django_session``) and the -:class:`django.contrib.admin.models.LogEntry` table (usually called +``django.contrib.admin.models.LogEntry`` table (usually called ``django_admin_log``). Those are the two standard tables that use :class:`~django.db.models.TextField` internally. diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index e67527de23..8d612ae6a6 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -292,6 +292,8 @@ Searches for and loads the contents of the named fixture into the database. The :djadminopt:`--database` option can be used to specify the database onto which the data will be loaded. +.. django-admin-option:: --ignorenonexistent + .. versionadded:: 1.5 The :djadminopt:`--ignorenonexistent` option can be used to ignore fields that diff --git a/docs/ref/exceptions.txt b/docs/ref/exceptions.txt index e91a5dd85e..f123ae2e59 100644 --- a/docs/ref/exceptions.txt +++ b/docs/ref/exceptions.txt @@ -131,6 +131,21 @@ The Django wrappers for database exceptions behave exactly the same as the underlying database exceptions. See :pep:`249`, the Python Database API Specification v2.0, for further information. +.. exception:: models.ProtectedError + +Raised to prevent deletion of referenced objects when using +:attr:`django.db.models.PROTECT`. Subclass of :exc:`IntegrityError`. + +.. currentmodule:: django.http + +Http Exceptions +=============== + +.. exception:: UnreadablePostError + + The :exc:`UnreadablePostError` is raised when a user cancels an upload. + It is available from :mod:`django.http`. + .. currentmodule:: django.db.transaction Transaction Exceptions diff --git a/docs/ref/files/file.txt b/docs/ref/files/file.txt index ada614df45..7562f9b6bf 100644 --- a/docs/ref/files/file.txt +++ b/docs/ref/files/file.txt @@ -14,7 +14,7 @@ The ``File`` Class The :class:`File` is a thin wrapper around Python's built-in file object with some Django-specific additions. Internally, Django uses this class any time it needs to represent a file. - + :class:`File` objects have the following attributes and methods: .. attribute:: name @@ -148,7 +148,7 @@ below) will also have a couple of extra methods: Note that the ``content`` argument must be an instance of either :class:`File` or of a subclass of :class:`File`, such as - :class:`ContentFile`. + :class:`~django.core.files.base.ContentFile`. .. method:: File.delete([save=True]) diff --git a/docs/ref/files/storage.txt b/docs/ref/files/storage.txt index f9bcf9b61e..ff175d122b 100644 --- a/docs/ref/files/storage.txt +++ b/docs/ref/files/storage.txt @@ -38,7 +38,7 @@ The FileSystemStorage Class .. note:: - The :class:`FileSystemStorage.delete` method will not raise + The ``FileSystemStorage.delete()`` method will not raise raise an exception if the given file name does not exist. The Storage Class diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index ab1f4b0eea..4aacbf0a0d 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -2,9 +2,7 @@ The Forms API ============= -.. module:: django.forms.forms - -.. currentmodule:: django.forms +.. module:: django.forms .. admonition:: About this document @@ -380,6 +378,9 @@ a form object, and each rendering method returns a Unicode object. Styling required or erroneous form rows ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. attribute:: Form.error_css_class +.. attribute:: Form.required_css_class + It's pretty common to style form rows and fields that are required or have errors. For example, you might want to present required form rows in bold and highlight errors in red. @@ -587,24 +588,24 @@ lazy developers -- they're not the only way a form object can be displayed. Used to display HTML or access attributes for a single field of a :class:`Form` instance. - The :meth:`__unicode__` and :meth:`__str__` methods of this object displays + The ``__unicode__()`` and ``__str__()`` methods of this object displays the HTML for this field. To retrieve a single ``BoundField``, use dictionary lookup syntax on your form using the field's name as the key:: - >>> form = ContactForm() - >>> print(form['subject']) - + >>> form = ContactForm() + >>> print(form['subject']) + To retrieve all ``BoundField`` objects, iterate the form:: - >>> form = ContactForm() - >>> for boundfield in form: print(boundfield) - - - - + >>> form = ContactForm() + >>> for boundfield in form: print(boundfield) + + + + The field-specific output honors the form object's ``auto_id`` setting:: @@ -635,7 +636,7 @@ For a field's list of errors, access the field's ``errors`` attribute. >>> print(f['subject'].errors) >>> str(f['subject'].errors) - '' + '' .. method:: BoundField.css_classes() @@ -644,17 +645,17 @@ indicate required form fields or fields that contain errors. If you're manually rendering a form, you can access these CSS classes using the ``css_classes`` method:: - >>> f = ContactForm(data) - >>> f['message'].css_classes() - 'required' + >>> f = ContactForm(data) + >>> f['message'].css_classes() + 'required' If you want to provide some additional classes in addition to the error and required classes that may be required, you can provide those classes as an argument:: - >>> f = ContactForm(data) - >>> f['message'].css_classes('foo bar') - 'foo bar required' + >>> f = ContactForm(data) + >>> f['message'].css_classes('foo bar') + 'foo bar required' .. method:: BoundField.value() diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index d8d9c9b770..bc1270094b 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -508,9 +508,9 @@ Selector and checkbox widgets .. attribute:: Select.choices - This attribute is optional when the field does not have a - :attr:`~Field.choices` attribute. If it does, it will override anything - you set here when the attribute is updated on the :class:`Field`. + This attribute is optional when the form field does not have a + ``choices`` attribute. If it does, it will override anything you set + here when the attribute is updated on the :class:`Field`. ``NullBooleanSelect`` ~~~~~~~~~~~~~~~~~~~~~ @@ -660,9 +660,9 @@ Composite widgets .. attribute:: MultipleHiddenInput.choices - This attribute is optional when the field does not have a - :attr:`~Field.choices` attribute. If it does, it will override anything - you set here when the attribute is updated on the :class:`Field`. + This attribute is optional when the form field does not have a + ``choices`` attribute. If it does, it will override anything you set + here when the attribute is updated on the :class:`Field`. ``SplitDateTimeWidget`` ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 31cc6f24f6..2b053d80ab 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -111,7 +111,7 @@ It will NOT compress content if any of the following are true: not to be performed on certain content types. You can apply GZip compression to individual views using the -:func:`~django.views.decorators.http.gzip_page()` decorator. +:func:`~django.views.decorators.gzip.gzip_page()` decorator. Conditional GET middleware -------------------------- @@ -124,7 +124,7 @@ Conditional GET middleware Handles conditional GET operations. If the response has a ``ETag`` or ``Last-Modified`` header, and the request has ``If-None-Match`` or ``If-Modified-Since``, the response is replaced by an -:class:`~django.http.HttpNotModified`. +:class:`~django.http.HttpResponseNotModified`. Also sets the ``Date`` and ``Content-Length`` response-headers. diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index e9f85e0657..6498b6c845 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -113,7 +113,7 @@ define a suitably-named constant for each value:: default=FRESHMAN) def is_upperclass(self): - return self.year_in_school in (self.JUNIOR, self.SENIOR) + return self.year_in_school in (self.JUNIOR, self.SENIOR) Though you can define a choices list outside of a model class and then refer to it, defining the choices and names for each choice inside the @@ -509,8 +509,8 @@ Has one **required** argument: .. attribute:: FileField.upload_to A local filesystem path that will be appended to your :setting:`MEDIA_ROOT` - setting to determine the value of the :attr:`~django.core.files.File.url` - attribute. + setting to determine the value of the + :attr:`~django.db.models.fields.files.FieldFile.url` attribute. This path may contain :func:`~time.strftime` formatting, which will be replaced by the date/time of the file upload (so that uploaded files don't @@ -564,9 +564,9 @@ takes a few steps: 3. All that will be stored in your database is a path to the file (relative to :setting:`MEDIA_ROOT`). You'll most likely want to use the - convenience :attr:`~django.core.files.File.url` function provided by - Django. For example, if your :class:`ImageField` is called ``mug_shot``, - you can get the absolute path to your image in a template with + convenience :attr:`~django.db.models.fields.files.FieldFile.url` attribute + provided by Django. For example, if your :class:`ImageField` is called + ``mug_shot``, you can get the absolute path to your image in a template with ``{{ object.mug_shot.url }}``. For example, say your :setting:`MEDIA_ROOT` is set to ``'/home/media'``, and @@ -589,7 +589,7 @@ topic guide. saved. The uploaded file's relative URL can be obtained using the -:attr:`~django.db.models.FileField.url` attribute. Internally, +:attr:`~django.db.models.fields.files.FieldFile.url` attribute. Internally, this calls the :meth:`~django.core.files.storage.Storage.url` method of the underlying :class:`~django.core.files.storage.Storage` class. @@ -614,9 +614,20 @@ can change the maximum length using the :attr:`~CharField.max_length` argument. FileField and FieldFile ~~~~~~~~~~~~~~~~~~~~~~~ -When you access a :class:`FileField` on a model, you are given an instance -of :class:`FieldFile` as a proxy for accessing the underlying file. This -class has several methods that can be used to interact with file data: +.. currentmodule:: django.db.models.fields.files + +.. class:: FieldFile + +When you access a :class:`~django.db.models.FileField` on a model, you are +given an instance of :class:`FieldFile` as a proxy for accessing the underlying +file. This class has several attributes and methods that can be used to +interact with file data: + +.. attribute:: FieldFile.url + +A read-only property to access the file's relative URL by calling the +:meth:`~django.core.files.storage.Storage.url` method of the underlying +:class:`~django.core.files.storage.Storage` class. .. method:: FieldFile.open(mode='rb') @@ -632,9 +643,9 @@ associated with this instance. This method takes a filename and file contents and passes them to the storage class for the field, then associates the stored file with the model field. -If you want to manually associate file data with :class:`FileField` -instances on your model, the ``save()`` method is used to persist that file -data. +If you want to manually associate file data with +:class:`~django.db.models.FileField` instances on your model, the ``save()`` +method is used to persist that file data. Takes two required arguments: ``name`` which is the name of the file, and ``content`` which is an object containing the file's contents. The @@ -672,6 +683,8 @@ to cleanup orphaned files, you'll need to handle it yourself (for instance, with a custom management command that can be run manually or scheduled to run periodically via e.g. cron). +.. currentmodule:: django.db.models + ``FilePathField`` ----------------- @@ -759,8 +772,7 @@ Inherits all attributes and methods from :class:`FileField`, but also validates that the uploaded object is a valid image. In addition to the special attributes that are available for :class:`FileField`, -an :class:`ImageField` also has :attr:`~django.core.files.File.height` and -:attr:`~django.core.files.File.width` attributes. +an :class:`ImageField` also has ``height`` and ``width`` attributes. To facilitate querying on those attributes, :class:`ImageField` has two extra optional arguments: @@ -1047,26 +1059,36 @@ define the details of how the relation works. user = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL) - The possible values for :attr:`on_delete` are found in - :mod:`django.db.models`: +The possible values for :attr:`~ForeignKey.on_delete` are found in +:mod:`django.db.models`: - * :attr:`~django.db.models.CASCADE`: Cascade deletes; the default. +* .. attribute:: CASCADE - * :attr:`~django.db.models.PROTECT`: Prevent deletion of the referenced - object by raising :exc:`django.db.models.ProtectedError`, a subclass of - :exc:`django.db.IntegrityError`. + Cascade deletes; the default. - * :attr:`~django.db.models.SET_NULL`: Set the :class:`ForeignKey` null; - this is only possible if :attr:`null` is ``True``. +* .. attribute:: PROTECT - * :attr:`~django.db.models.SET_DEFAULT`: Set the :class:`ForeignKey` to its - default value; a default for the :class:`ForeignKey` must be set. + Prevent deletion of the referenced object by raising + :exc:`~django.db.models.ProtectedError`, a subclass of + :exc:`django.db.IntegrityError`. - * :func:`~django.db.models.SET()`: Set the :class:`ForeignKey` to the value - passed to :func:`~django.db.models.SET()`, or if a callable is passed in, - the result of calling it. In most cases, passing a callable will be - necessary to avoid executing queries at the time your models.py is - imported:: +* .. attribute:: SET_NULL + + Set the :class:`ForeignKey` null; this is only possible if + :attr:`~Field.null` is ``True``. + +* .. attribute:: SET_DEFAULT + + Set the :class:`ForeignKey` to its default value; a default for the + :class:`ForeignKey` must be set. + +* .. function:: SET() + + Set the :class:`ForeignKey` to the value passed to + :func:`~django.db.models.SET()`, or if a callable is passed in, + the result of calling it. In most cases, passing a callable will be + necessary to avoid executing queries at the time your models.py is + imported:: def get_sentinel_user(): return User.objects.get_or_create(username='deleted')[0] @@ -1074,11 +1096,12 @@ define the details of how the relation works. class MyModel(models.Model): user = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user)) - * :attr:`~django.db.models.DO_NOTHING`: Take no action. If your database - backend enforces referential integrity, this will cause an - :exc:`~django.db.IntegrityError` unless you manually add a SQL ``ON - DELETE`` constraint to the database field (perhaps using - :ref:`initial sql`). +* .. attribute:: DO_NOTHING + + Take no action. If your database backend enforces referential + integrity, this will cause an :exc:`~django.db.IntegrityError` unless + you manually add a SQL ``ON DELETE`` constraint to the database field + (perhaps using :ref:`initial sql`). .. _ref-manytomany: diff --git a/docs/ref/models/options.txt b/docs/ref/models/options.txt index 6fd707fdf2..b349197a5b 100644 --- a/docs/ref/models/options.txt +++ b/docs/ref/models/options.txt @@ -100,7 +100,7 @@ Django quotes column and table names behind the scenes. .. attribute:: Options.managed Defaults to ``True``, meaning Django will create the appropriate database - tables in :djadmin:`syncdb` and remove them as part of a :djadmin:`reset` + tables in :djadmin:`syncdb` and remove them as part of a :djadmin:`flush` management command. That is, Django *manages* the database tables' lifecycles. diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index a8e0ef3f51..2b4397a138 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -263,7 +263,7 @@ Methods .. method:: HttpRequest.get_signed_cookie(key, default=RAISE_ERROR, salt='', max_age=None) Returns a cookie value for a signed cookie, or raises a - :class:`~django.core.signing.BadSignature` exception if the signature is + ``django.core.signing.BadSignature`` exception if the signature is no longer valid. If you provide the ``default`` argument the exception will be suppressed and that default value will be returned instead. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index bfe283cc68..be21f06de7 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2159,7 +2159,7 @@ startproject ` management command will create a simple ``wsgi.py`` file with an ``application`` callable in it, and point this setting to that ``application``. -If not set, the return value of :func:`django.core.wsgi.get_wsgi_application` +If not set, the return value of ``django.core.wsgi.get_wsgi_application()`` will be used. In this case, the behavior of :djadmin:`runserver` will be identical to previous Django versions. diff --git a/docs/ref/signals.txt b/docs/ref/signals.txt index f2f1459bf0..0995789391 100644 --- a/docs/ref/signals.txt +++ b/docs/ref/signals.txt @@ -436,9 +436,8 @@ Sent when Django begins processing an HTTP request. Arguments sent with this signal: ``sender`` - The handler class -- e.g. - :class:`django.core.handlers.wsgi.WsgiHandler` -- that handled - the request. + The handler class -- e.g. ``django.core.handlers.wsgi.WsgiHandler`` -- that + handled the request. request_finished ---------------- @@ -496,7 +495,7 @@ setting_changed :module: This signal is sent when the value of a setting is changed through the -:meth:`django.test.TestCase.setting` context manager or the +``django.test.TestCase.settings()`` context manager or the :func:`django.test.utils.override_settings` decorator/context manager. It's actually sent twice: when the new value is applied ("setup") and when the @@ -558,8 +557,8 @@ Arguments sent with this signal: ``sender`` The database wrapper class -- i.e. - :class:`django.db.backends.postgresql_psycopg2.DatabaseWrapper` or - :class:`django.db.backends.mysql.DatabaseWrapper`, etc. + ``django.db.backends.postgresql_psycopg2.DatabaseWrapper`` or + ``django.db.backends.mysql.DatabaseWrapper``, etc. ``connection`` The database connection that was opened. This can be used in a diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index d9b7130362..3f5e772737 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -121,15 +121,14 @@ Methods used as the response instead of the original response object (and will be passed to the next post rendering callback etc.) -.. method:: SimpleTemplateResponse.render(): +.. method:: SimpleTemplateResponse.render() - Sets :attr:`response.content` to the result obtained by + Sets ``response.content`` to the result obtained by :attr:`SimpleTemplateResponse.rendered_content`, runs all post-rendering callbacks, and returns the resulting response object. - :meth:`~SimpleTemplateResponse.render()` will only have an effect - the first time it is called. On subsequent calls, it will return - the result obtained from the first call. + ``render()`` will only have an effect the first time it is called. On + subsequent calls, it will return the result obtained from the first call. TemplateResponse objects @@ -188,24 +187,23 @@ returned to the client, it must be rendered. The rendering process takes the intermediate representation of template and context, and turns it into the final byte stream that can be served to the client. -There are three circumstances under which a TemplateResponse will be +There are three circumstances under which a ``TemplateResponse`` will be rendered: -* When the TemplateResponse instance is explicitly rendered, using +* When the ``TemplateResponse`` instance is explicitly rendered, using the :meth:`SimpleTemplateResponse.render()` method. * When the content of the response is explicitly set by assigning - :attr:`response.content`. + ``response.content``. * After passing through template response middleware, but before passing through response middleware. -A TemplateResponse can only be rendered once. The first call to -:meth:`SimpleTemplateResponse.render` sets the content of the -response; subsequent rendering calls do not change the response -content. +A ``TemplateResponse`` can only be rendered once. The first call to +:meth:`SimpleTemplateResponse.render` sets the content of the response; +subsequent rendering calls do not change the response content. -However, when :attr:`response.content` is explicitly assigned, the +However, when ``response.content`` is explicitly assigned, the change is always applied. If you want to force the content to be re-rendered, you can re-evaluate the rendered content, and assign the content of the response manually:: diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 7c17f0a758..0162f78eed 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -557,15 +557,17 @@ Note that these paths should use Unix-style forward slashes, even on Windows. The Python API ~~~~~~~~~~~~~~ -Django has two ways to load templates from files: +.. module:: django.template.loader -.. function:: django.template.loader.get_template(template_name) +``django.template.loader`` has two functions to load templates from files: + +.. function:: get_template(template_name) ``get_template`` returns the compiled template (a ``Template`` object) for the template with the given name. If the template doesn't exist, it raises ``django.template.TemplateDoesNotExist``. -.. function:: django.template.loader.select_template(template_name_list) +.. function:: select_template(template_name_list) ``select_template`` is just like ``get_template``, except it takes a list of template names. Of the list, it returns the first template that exists. @@ -630,11 +632,19 @@ by editing your :setting:`TEMPLATE_LOADERS` setting. :setting:`TEMPLATE_LOADERS` should be a tuple of strings, where each string represents a template loader class. Here are the template loaders that come with Django: +.. currentmodule:: django.template.loaders + ``django.template.loaders.filesystem.Loader`` + +.. class:: filesystem.Loader + Loads templates from the filesystem, according to :setting:`TEMPLATE_DIRS`. This loader is enabled by default. ``django.template.loaders.app_directories.Loader`` + +.. class:: app_directories.Loader + Loads templates from Django apps on the filesystem. For each app in :setting:`INSTALLED_APPS`, the loader looks for a ``templates`` subdirectory. If the directory exists, Django looks for templates in there. @@ -669,12 +679,18 @@ class. Here are the template loaders that come with Django: This loader is enabled by default. ``django.template.loaders.eggs.Loader`` + +.. class:: eggs.Loader + Just like ``app_directories`` above, but it loads templates from Python eggs rather than from the filesystem. This loader is disabled by default. ``django.template.loaders.cached.Loader`` + +.. class:: cached.Loader + By default, the templating system will read and compile your templates every time they need to be rendered. While the Django templating system is quite fast, the overhead from reading and compiling templates can add up. diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index aab53aed0c..cfc57cc551 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -377,7 +377,7 @@ block are output:: In the above, if ``athlete_list`` is not empty, the number of athletes will be displayed by the ``{{ athlete_list|length }}`` variable. -As you can see, the ``if`` tag may take one or several `` {% elif %}`` +As you can see, the ``if`` tag may take one or several ``{% elif %}`` clauses, as well as an ``{% else %}`` clause that will be displayed if all previous conditions fail. These clauses are optional. diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index 5a0b04f9fa..92b41b8fea 100644 --- a/docs/ref/urls.txt +++ b/docs/ref/urls.txt @@ -86,7 +86,6 @@ include() application and instance namespaces. :arg module: URLconf module (or module name) - :type module: Module or string :arg namespace: Instance namespace for the URL entries being included :type namespace: string :arg app_name: Application namespace for the URL entries being included @@ -142,4 +141,3 @@ value should suffice. See the documentation about :ref:`the 500 (HTTP Internal Server Error) view ` for more information. - diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 942cac2650..de805173d7 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -190,8 +190,7 @@ The functions defined in this module share the following properties: Like ``decorator_from_middleware``, but returns a function that accepts the arguments to be passed to the middleware_class. For example, the :func:`~django.views.decorators.cache.cache_page` - decorator is created from the - :class:`~django.middleware.cache.CacheMiddleware` like this:: + decorator is created from the ``CacheMiddleware`` like this:: cache_page = decorator_from_middleware_with_args(CacheMiddleware) @@ -282,15 +281,15 @@ The functions defined in this module share the following properties: .. function:: smart_str(s, encoding='utf-8', strings_only=False, errors='strict') Alias of :func:`smart_bytes` on Python 2 and :func:`smart_text` on Python - 3. This function returns a :class:`str` or a lazy string. + 3. This function returns a ``str`` or a lazy string. - For instance, this is suitable for writing to :attr:`sys.stdout` on + For instance, this is suitable for writing to :data:`sys.stdout` on Python 2 and 3. .. function:: force_str(s, encoding='utf-8', strings_only=False, errors='strict') Alias of :func:`force_bytes` on Python 2 and :func:`force_text` on Python - 3. This function always returns a :class:`str`. + 3. This function always returns a ``str``. .. function:: iri_to_uri(iri) @@ -624,12 +623,12 @@ escaping HTML. .. function:: base36_to_int(s) Converts a base 36 string to an integer. On Python 2 the output is - guaranteed to be an :class:`int` and not a :class:`long`. + guaranteed to be an ``int`` and not a ``long``. .. function:: int_to_base36(i) Converts a positive integer to a base 36 string. On Python 2 ``i`` must be - smaller than :attr:`sys.maxint`. + smaller than :data:`sys.maxint`. ``django.utils.safestring`` =========================== @@ -647,12 +646,12 @@ appropriate entities. .. versionadded:: 1.5 - A :class:`bytes` subclass that has been specifically marked as "safe" + A ``bytes`` subclass that has been specifically marked as "safe" (requires no further escaping) for HTML output purposes. .. class:: SafeString - A :class:`str` subclass that has been specifically marked as "safe" + A ``str`` subclass that has been specifically marked as "safe" (requires no further escaping) for HTML output purposes. This is :class:`SafeBytes` on Python 2 and :class:`SafeText` on Python 3. @@ -660,7 +659,7 @@ appropriate entities. .. versionadded:: 1.5 - A :class:`str` (in Python 3) or :class:`unicode` (in Python 2) subclass + A ``str`` (in Python 3) or ``unicode`` (in Python 2) subclass that has been specifically marked as "safe" for HTML output purposes. .. class:: SafeUnicode diff --git a/docs/ref/validators.txt b/docs/ref/validators.txt index 0536b03d64..8da134a42d 100644 --- a/docs/ref/validators.txt +++ b/docs/ref/validators.txt @@ -118,7 +118,7 @@ to, or in lieu of custom ``field.clean()`` methods. .. data:: validate_ipv6_address - Uses :mod:`django.utils.ipv6` to check the validity of an IPv6 address. + Uses ``django.utils.ipv6`` to check the validity of an IPv6 address. ``validate_ipv46_address`` -------------------------- diff --git a/docs/releases/1.2-beta-1.txt b/docs/releases/1.2-beta-1.txt index 3549767379..abb0f3bbb9 100644 --- a/docs/releases/1.2-beta-1.txt +++ b/docs/releases/1.2-beta-1.txt @@ -47,7 +47,7 @@ should be updated to use the new :ref:`class-based runners Syndication feeds ----------------- -The :class:`django.contrib.syndication.feeds.Feed` class is being +The ``django.contrib.syndication.feeds.Feed`` class is being replaced by the :class:`django.contrib.syndication.views.Feed` class. The old ``feeds.Feed`` class is deprecated. The new class has an almost identical API, but allows instances to be used as views. diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 68cec91587..50c049f5da 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -345,10 +345,10 @@ in 1.2 is support for multiple spatial databases. As a result, the following :ref:`spatial database backends ` are now included: -* :mod:`django.contrib.gis.db.backends.postgis` -* :mod:`django.contrib.gis.db.backends.mysql` -* :mod:`django.contrib.gis.db.backends.oracle` -* :mod:`django.contrib.gis.db.backends.spatialite` +* ``django.contrib.gis.db.backends.postgis`` +* ``django.contrib.gis.db.backends.mysql`` +* ``django.contrib.gis.db.backends.oracle`` +* ``django.contrib.gis.db.backends.spatialite`` GeoDjango now supports the rich capabilities added in the `PostGIS 1.5 release `_. @@ -986,7 +986,7 @@ should be updated to use the new :ref:`class-based runners ``Feed`` in ``django.contrib.syndication.feeds`` ------------------------------------------------ -The :class:`django.contrib.syndication.feeds.Feed` class has been +The ``django.contrib.syndication.feeds.Feed`` class has been replaced by the :class:`django.contrib.syndication.views.Feed` class. The old ``feeds.Feed`` class is deprecated, and will be removed in Django 1.4. diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index e2c52a7264..ba8a4fc557 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -150,7 +150,7 @@ process has been on adding lots of smaller, long standing feature requests. These include: * Improved tools for accessing and manipulating the current Site via - :func:`django.contrib.sites.models.get_current_site`. + ``django.contrib.sites.models.get_current_site()``. * A :class:`~django.test.client.RequestFactory` for mocking requests in tests. diff --git a/docs/releases/1.3-beta-1.txt b/docs/releases/1.3-beta-1.txt index d064063fce..14897ed3b7 100644 --- a/docs/releases/1.3-beta-1.txt +++ b/docs/releases/1.3-beta-1.txt @@ -140,7 +140,7 @@ attribute. Changes to ``USStateField`` =========================== -The :mod:`django.contrib.localflavor` application contains collections +The ``django.contrib.localflavor`` application contains collections of code relevant to specific countries or cultures. One such is ``USStateField``, which provides a field for storing the two-letter postal abbreviation of a U.S. state. This field has consistently caused problems, @@ -167,13 +167,13 @@ as a pair of changes: independent nations -- the Federated States of Micronesia, the Republic of the Marshall Islands and the Republic of Palau -- which are serviced under treaty by the U.S. postal system. A new form - widget, :class:`django.contrib.localflavor.us.forms.USPSSelect`, is + widget, ``django.contrib.localflavor.us.forms.USPSSelect``, is also available and provides the same set of choices. Additionally, several finer-grained choice tuples are provided which allow mixing and matching of subsets of the U.S. states and territories, and other locations serviced by the U.S. postal -system. Consult the :mod:`django.contrib.localflavor` documentation +system. Consult the ``django.contrib.localflavor`` documentation for more details. The change to `USStateField` is technically backwards-incompatible for diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 6a056532b9..4c8dd2f81f 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -367,9 +367,8 @@ In earlier Django versions, when a model instance containing a file from the backend storage. This opened the door to several data-loss scenarios, including rolled-back transactions and fields on different models referencing the same file. In Django 1.3, when a model is deleted the -:class:`~django.db.models.FileField`'s -:func:`~django.db.models.FileField.delete` method won't be called. If you -need cleanup of orphaned files, you'll need to handle it yourself (for +:class:`~django.db.models.FileField`'s ``delete()`` method won't be called. If +you need cleanup of orphaned files, you'll need to handle it yourself (for instance, with a custom management command that can be run manually or scheduled to run periodically via e.g. cron). diff --git a/docs/releases/1.4-alpha-1.txt b/docs/releases/1.4-alpha-1.txt index 4086cfdecc..09855400eb 100644 --- a/docs/releases/1.4-alpha-1.txt +++ b/docs/releases/1.4-alpha-1.txt @@ -504,7 +504,7 @@ Django 1.4 also includes several smaller improvements worth noting: page. * The ``django.contrib.auth.models.check_password`` function has been moved - to the :mod:`django.contrib.auth.utils` module. Importing it from the old + to the ``django.contrib.auth.utils`` module. Importing it from the old location will still work, but you should update your imports. * The :djadmin:`collectstatic` management command gained a ``--clear`` option diff --git a/docs/releases/1.4-beta-1.txt b/docs/releases/1.4-beta-1.txt index a8732a9e65..8ea63742e3 100644 --- a/docs/releases/1.4-beta-1.txt +++ b/docs/releases/1.4-beta-1.txt @@ -564,7 +564,7 @@ Django 1.4 also includes several smaller improvements worth noting: page. * The ``django.contrib.auth.models.check_password`` function has been moved - to the :mod:`django.contrib.auth.utils` module. Importing it from the old + to the ``django.contrib.auth.utils`` module. Importing it from the old location will still work, but you should update your imports. * The :djadmin:`collectstatic` management command gained a ``--clear`` option diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index cf53b37f17..9459e940b4 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -888,10 +888,10 @@ object, Django raises an exception. ``MySQLdb``-specific exceptions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The MySQL backend historically has raised :class:`MySQLdb.OperationalError` +The MySQL backend historically has raised ``MySQLdb.OperationalError`` when a query triggered an exception. We've fixed this bug, and we now raise :exc:`django.db.DatabaseError` instead. If you were testing for -:class:`MySQLdb.OperationalError`, you'll need to update your ``except`` +``MySQLdb.OperationalError``, you'll need to update your ``except`` clauses. Database connection's thread-locality diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index 0f44444416..76284ae72f 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -171,18 +171,18 @@ Manually managing a user's password .. module:: django.contrib.auth.hashers - The :mod:`django.contrib.auth.hashers` module provides a set of functions - to create and validate hashed password. You can use them independently - from the ``User`` model. +The :mod:`django.contrib.auth.hashers` module provides a set of functions +to create and validate hashed password. You can use them independently +from the ``User`` model. .. function:: check_password(password, encoded) If you'd like to manually authenticate a user by comparing a plain-text password to the hashed password in the database, use the convenience - function :func:`django.contrib.auth.hashers.check_password`. It takes two - arguments: the plain-text password to check, and the full value of a - user's ``password`` field in the database to check against, and returns - ``True`` if they match, ``False`` otherwise. + function :func:`check_password`. It takes two arguments: the plain-text + password to check, and the full value of a user's ``password`` field in the + database to check against, and returns ``True`` if they match, ``False`` + otherwise. .. function:: make_password(password[, salt, hashers]) @@ -195,9 +195,9 @@ Manually managing a user's password ``'unsalted_md5'`` (only for backward compatibility) and ``'crypt'`` if you have the ``crypt`` library installed. If the password argument is ``None``, an unusable password is returned (a one that will be never - accepted by :func:`django.contrib.auth.hashers.check_password`). + accepted by :func:`check_password`). .. function:: is_password_usable(encoded_password) Checks if the given string is a hashed password that has a chance - of being verified against :func:`django.contrib.auth.hashers.check_password`. + of being verified against :func:`check_password`. diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index 9b3e41d0d4..208fa3a5e2 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -664,6 +664,8 @@ pickling.) Accessing the cache ------------------- +.. function:: django.core.cache.get_cache(backend, **kwargs) + The cache module, ``django.core.cache``, has a ``cache`` object that's automatically created from the ``'default'`` entry in the :setting:`CACHES` setting:: @@ -676,7 +678,7 @@ If you have multiple caches defined in :setting:`CACHES`, then you can use >>> from django.core.cache import get_cache >>> cache = get_cache('alternate') -If the named key does not exist, :exc:`InvalidCacheBackendError` will be raised. +If the named key does not exist, ``InvalidCacheBackendError`` will be raised. Basic usage @@ -844,7 +846,7 @@ key version to set or get. For example:: 'hello world!' The version of a specific key can be incremented and decremented using -the :func:`incr_version()` and :func:`decr_version()` methods. This +the ``incr_version()`` and ``decr_version()`` methods. This enables specific keys to be bumped to a new version, leaving other keys unaffected. Continuing our previous example:: @@ -879,7 +881,7 @@ parts), you can provide a custom key function. The :setting:`KEY_FUNCTION ` cache setting specifies a dotted-path to a function matching the prototype of -:func:`make_key()` above. If provided, this custom key function will +``make_key()`` above. If provided, this custom key function will be used instead of the default key combining function. Cache key warnings diff --git a/docs/topics/class-based-views/generic-display.txt b/docs/topics/class-based-views/generic-display.txt index 10279c0f63..dac45c8843 100644 --- a/docs/topics/class-based-views/generic-display.txt +++ b/docs/topics/class-based-views/generic-display.txt @@ -257,9 +257,9 @@ Specifying ``model = Publisher`` is really just shorthand for saying ``queryset = Publisher.objects.all()``. However, by using ``queryset`` to define a filtered list of objects you can be more specific about the objects that will be visible in the view (see :doc:`/topics/db/queries` -for more information about :class:`QuerySet` objects, and see the -:doc:`class-based views reference ` for the -complete details). +for more information about :class:`~django.db.models.query.QuerySet` objects, +and see the :doc:`class-based views reference ` +for the complete details). To pick a simple example, we might want to order a list of books by publication date, with the most recent first:: @@ -312,9 +312,9 @@ what if we wanted to write a view that displayed all the books by some arbitrary publisher? Handily, the ``ListView`` has a -:meth:`~django.views.generic.detail.ListView.get_queryset` method we can -override. Previously, it has just been returning the value of the ``queryset`` -attribute, but now we can add more logic. +:meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset` method we +can override. Previously, it has just been returning the value of the +``queryset`` attribute, but now we can add more logic. The key part to making this work is that when class-based views are called, various useful things are stored on ``self``; as well as the request diff --git a/docs/topics/class-based-views/generic-editing.txt b/docs/topics/class-based-views/generic-editing.txt index 7d12184705..2f8b8b0711 100644 --- a/docs/topics/class-based-views/generic-editing.txt +++ b/docs/topics/class-based-views/generic-editing.txt @@ -7,10 +7,10 @@ Form processing generally has 3 paths: * POST with invalid data (typically redisplay form with errors) * POST with valid data (process the data and typically redirect) -Implementing this yourself often results in a lot of repeated -boilerplate code (see :ref:`Using a form in a -view`). To help avoid this, Django provides a -collection of generic class-based views for form processing. +Implementing this yourself often results in a lot of repeated boilerplate code +(see :ref:`Using a form in a view`). To help avoid +this, Django provides a collection of generic class-based views for form +processing. Basic Forms ----------- @@ -28,7 +28,7 @@ Given a simple contact form:: # send email using the self.cleaned_data dictionary pass -The view can be constructed using a FormView:: +The view can be constructed using a ``FormView``:: # views.py from myapp.forms import ContactForm @@ -50,42 +50,46 @@ Notes: * FormView inherits :class:`~django.views.generic.base.TemplateResponseMixin` so :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` - can be used here + can be used here. * The default implementation for - :meth:`~django.views.generic.edit.FormView.form_valid` simply - redirects to the :attr:`success_url` + :meth:`~django.views.generic.edit.FormMixin.form_valid` simply + redirects to the :attr:`~django.views.generic.edit.FormMixin.success_url`. Model Forms ----------- Generic views really shine when working with models. These generic -views will automatically create a :class:`ModelForm`, so long as they -can work out which model class to use: - -* If the :attr:`model` attribute is given, that model class will be used -* If :meth:`get_object()` returns an object, the class of that object - will be used -* If a :attr:`queryset` is given, the model for that queryset will be used - -Model form views provide a :meth:`form_valid()` implementation that -saves the model automatically. You can override this if you have any +views will automatically create a :class:`~django.forms.ModelForm`, so long as +they can work out which model class to use: + +* If the :attr:`~django.views.generic.edit.ModelFormMixin.model` attribute is + given, that model class will be used. +* If :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()` + returns an object, the class of that object will be used. +* If a :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` is + given, the model for that queryset will be used. + +Model form views provide a +:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` implementation +that saves the model automatically. You can override this if you have any special requirements; see below for examples. -You don't even need to provide a attr:`success_url` for +You don't even need to provide a ``success_url`` for :class:`~django.views.generic.edit.CreateView` or :class:`~django.views.generic.edit.UpdateView` - they will use -:meth:`get_absolute_url()` on the model object if available. +:meth:`~django.db.models.Model.get_absolute_url()` on the model object if available. -If you want to use a custom :class:`ModelForm` (for instance to add -extra validation) simply set +If you want to use a custom :class:`~django.forms.ModelForm` (for instance to +add extra validation) simply set :attr:`~django.views.generic.edit.FormMixin.form_class` on your view. .. note:: When specifying a custom form class, you must still specify the model, - even though the :attr:`form_class` may be a :class:`ModelForm`. + even though the :attr:`~django.views.generic.edit.FormMixin.form_class` may + be a :class:`~django.forms.ModelForm`. -First we need to add :meth:`get_absolute_url()` to our :class:`Author` -class: +First we need to add :meth:`~django.db.models.Model.get_absolute_url()` to our +``Author`` class: .. code-block:: python @@ -137,8 +141,10 @@ Finally, we hook these new views into the URLconf:: .. note:: - These views inherit :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` - which uses :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_prefix` + These views inherit + :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` + which uses + :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` to construct the :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` based on the model. @@ -149,15 +155,17 @@ Finally, we hook these new views into the URLconf:: * :class:`DeleteView` uses ``myapp/author_confirm_delete.html`` If you wish to have separate templates for :class:`CreateView` and - :class:1UpdateView`, you can set either :attr:`template_name` or - :attr:`template_name_suffix` on your view class. + :class:`UpdateView`, you can set either + :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` or + :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` + on your view class. Models and request.user ----------------------- To track the user that created an object using a :class:`CreateView`, -you can use a custom :class:`ModelForm` to do this. First, add the -foreign key relation to the model:: +you can use a custom :class:`~django.forms.ModelForm` to do this. First, add +the foreign key relation to the model:: # models.py from django.contrib.auth import User @@ -169,7 +177,7 @@ foreign key relation to the model:: # ... -Create a custom :class:`ModelForm` in order to exclude the +Create a custom :class:`~django.forms.ModelForm` in order to exclude the ``created_by`` field and prevent the user from editing it: .. code-block:: python @@ -183,8 +191,10 @@ Create a custom :class:`ModelForm` in order to exclude the model = Author exclude = ('created_by',) -In the view, use the custom :attr:`form_class` and override -:meth:`form_valid()` to add the user:: +In the view, use the custom +:attr:`~django.views.generic.edit.FormMixin.form_class` and override +:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` to add the +user:: # views.py from django.views.generic.edit import CreateView @@ -202,7 +212,8 @@ In the view, use the custom :attr:`form_class` and override Note that you'll need to :ref:`decorate this view` using :func:`~django.contrib.auth.decorators.login_required`, or -alternatively handle unauthorised users in the :meth:`form_valid()`. +alternatively handle unauthorized users in the +:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()`. AJAX example ------------ diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 923b877cc5..4941ea9755 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -32,13 +32,14 @@ Two central mixins are provided that help in providing a consistent interface to working with templates in class-based views. :class:`~django.views.generic.base.TemplateResponseMixin` + Every built in view which returns a :class:`~django.template.response.TemplateResponse` will call the :meth:`~django.views.generic.base.TemplateResponseMixin.render_to_response` - method that :class:`TemplateResponseMixin` provides. Most of the time this + method that ``TemplateResponseMixin`` provides. Most of the time this will be called for you (for instance, it is called by the ``get()`` method implemented by both :class:`~django.views.generic.base.TemplateView` and - :class:`~django.views.generic.base.DetailView`); similarly, it's unlikely + :class:`~django.views.generic.detail.DetailView`); similarly, it's unlikely that you'll need to override it, although if you want your response to return something not rendered via a Django template then you'll want to do it. For an example of this, see the :ref:`JSONResponseMixin example @@ -59,10 +60,10 @@ interface to working with templates in class-based views. :class:`~django.views.generic.base.ContextMixin` Every built in view which needs context data, such as for rendering a - template (including :class:`TemplateResponseMixin` above), should call + template (including ``TemplateResponseMixin`` above), should call :meth:`~django.views.generic.base.ContextMixin.get_context_data` passing any data they want to ensure is in there as keyword arguments. - ``get_context_data`` returns a dictionary; in :class:`ContextMixin` it + ``get_context_data`` returns a dictionary; in ``ContextMixin`` it simply returns its keyword arguments, but it is common to override this to add more members to the dictionary. @@ -106,7 +107,7 @@ URLConf, and looks the object up either from the :attr:`~django.views.generic.detail.SingleObjectMixin.model` attribute on the view, or the :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` -attribute if that's provided). :class:`SingleObjectMixin` also overrides +attribute if that's provided). ``SingleObjectMixin`` also overrides :meth:`~django.views.generic.base.ContextMixin.get_context_data`, which is used across all Django's built in class-based views to supply context data for template renders. @@ -115,10 +116,12 @@ To then make a :class:`~django.template.response.TemplateResponse`, :class:`DetailView` uses :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, which extends :class:`~django.views.generic.base.TemplateResponseMixin`, -overriding :meth:`get_template_names()` as discussed above. It actually -provides a fairly sophisticated set of options, but the main one that most -people are going to use is ``/_detail.html``. The -``_detail`` part can be changed by setting +overriding +:meth:`~django.views.generic.base.TemplateResponseMixin.get_template_names()` +as discussed above. It actually provides a fairly sophisticated set of options, +but the main one that most people are going to use is +``/_detail.html``. The ``_detail`` part can be changed +by setting :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix` on a subclass to something else. (For instance, the :doc:`generic edit views` use ``_form`` for create and update views, and @@ -128,9 +131,10 @@ ListView: working with many Django objects ------------------------------------------ Lists of objects follow roughly the same pattern: we need a (possibly -paginated) list of objects, typically a :class:`QuerySet`, and then we need -to make a :class:`TemplateResponse` with a suitable template using -that list of objects. +paginated) list of objects, typically a +:class:`~django.db.models.query.QuerySet`, and then we need to make a +:class:`~django.template.response.TemplateResponse` with a suitable template +using that list of objects. To get the objects, :class:`~django.views.generic.list.ListView` uses :class:`~django.views.generic.list.MultipleObjectMixin`, which @@ -138,9 +142,9 @@ provides both :meth:`~django.views.generic.list.MultipleObjectMixin.get_queryset` and :meth:`~django.views.generic.list.MultipleObjectMixin.paginate_queryset`. Unlike -with :class:`SingleObjectMixin`, there's no need to key off parts of -the URL to figure out the queryset to work with, so the default just -uses the +with :class:`~django.views.generic.detail.SingleObjectMixin`, there's no need +to key off parts of the URL to figure out the queryset to work with, so the +default just uses the :attr:`~django.views.generic.list.MultipleObjectMixin.queryset` or :attr:`~django.views.generic.list.MultipleObjectMixin.model` attribute on the view class. A common reason to override @@ -148,19 +152,19 @@ on the view class. A common reason to override here would be to dynamically vary the objects, such as depending on the current user or to exclude posts in the future for a blog. -:class:`MultipleObjectMixin` also overrides +:class:`~django.views.generic.list.MultipleObjectMixin` also overrides :meth:`~django.views.generic.base.ContextMixin.get_context_data` to include appropriate context variables for pagination (providing dummies if pagination is disabled). It relies on ``object_list`` being passed in as a keyword argument, which :class:`ListView` arranges for it. -To make a :class:`TemplateResponse`, :class:`ListView` then uses +To make a :class:`~django.template.response.TemplateResponse`, +:class:`ListView` then uses :class:`~django.views.generic.list.MultipleObjectTemplateResponseMixin`; -as with :class:`SingleObjectTemplateResponseMixin` above, this -overrides :meth:`get_template_names()` to provide :meth:`a range of -options -<~django.views.generic.list.MultipleObjectTempalteResponseMixin>`, +as with :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin` +above, this overrides ``get_template_names()`` to provide :meth:`a range of +options `, with the most commonly-used being ``/_list.html``, with the ``_list`` part again being taken from the @@ -197,13 +201,13 @@ the box. If in doubt, it's often better to back off and base your work on :class:`View` or :class:`TemplateView`, perhaps with - :class:`SimpleObjectMixin` and - :class:`MultipleObjectMixin`. Although you will probably end up - writing more code, it is more likely to be clearly understandable - to someone else coming to it later, and with fewer interactions to - worry about you will save yourself some thinking. (Of course, you - can always dip into Django's implementation of the generic class - based views for inspiration on how to tackle problems.) + :class:`~django.views.generic.detail.SingleObjectMixin` and + :class:`~django.views.generic.list.MultipleObjectMixin`. Although you + will probably end up writing more code, it is more likely to be clearly + understandable to someone else coming to it later, and with fewer + interactions to worry about you will save yourself some thinking. (Of + course, you can always dip into Django's implementation of the generic + class based views for inspiration on how to tackle problems.) .. _method resolution order: http://www.python.org/download/releases/2.3/mro/ @@ -247,9 +251,9 @@ We'll demonstrate this with the publisher modelling we used in the In practice you'd probably want to record the interest in a key-value store rather than in a relational database, so we've left that bit out. The only bit of the view that needs to worry about using -:class:`SingleObjectMixin` is where we want to look up the author -we're interested in, which it just does with a simple call to -``self.get_object()``. Everything else is taken care of for us by the +:class:`~django.views.generic.detail.SingleObjectMixin` is where we want to +look up the author we're interested in, which it just does with a simple call +to ``self.get_object()``. Everything else is taken care of for us by the mixin. We can hook this into our URLs easily enough:: @@ -265,7 +269,8 @@ We can hook this into our URLs easily enough:: Note the ``pk`` named group, which :meth:`~django.views.generic.detail.SingleObjectMixin.get_object` uses to look up the ``Author`` instance. You could also use a slug, or -any of the other features of :class:`SingleObjectMixin`. +any of the other features of +:class:`~django.views.generic.detail.SingleObjectMixin`. Using SingleObjectMixin with ListView ------------------------------------- @@ -277,23 +282,24 @@ example, you might want to paginate through all the books by a particular publisher. One way to do this is to combine :class:`ListView` with -:class:`SingleObjectMixin`, so that the queryset for the paginated -list of books can hang off the publisher found as the single +:class:`~django.views.generic.detail.SingleObjectMixin`, so that the queryset +for the paginated list of books can hang off the publisher found as the single object. In order to do this, we need to have two different querysets: **Publisher queryset for use in get_object** - We'll set that up directly when we call :meth:`get_object()`. + We'll set that up directly when we call ``get_object()``. **Book queryset for use by ListView** - We'll figure that out ourselves in :meth:`get_queryset()` so we - can take into account the Publisher we're looking at. + We'll figure that out ourselves in ``get_queryset()`` so we + can take into account the ``Publisher`` we're looking at. .. note:: - We have to think carefully about :meth:`get_context_data()`. - Since both :class:`SingleObjectMixin` and :class:`ListView` will + We have to think carefully about ``get_context_data()``. + Since both :class:`~django.views.generic.detail.SingleObjectMixin` and + :class:`ListView` will put things in the context data under the value of - :attr:`context_object_name` if it's set, we'll instead explictly + ``context_object_name`` if it's set, we'll instead explictly ensure the Publisher is in the context data. :class:`ListView` will add in the suitable ``page_obj`` and ``paginator`` for us providing we remember to call ``super()``. @@ -316,13 +322,14 @@ Now we can write a new ``PublisherDetail``:: self.object = self.get_object(Publisher.objects.all()) return self.object.book_set.all() -Notice how we set ``self.object`` within :meth:`get_queryset` so we -can use it again later in :meth:`get_context_data`. If you don't set -:attr:`template_name`, the template will default to the normal +Notice how we set ``self.object`` within ``get_queryset()`` so we +can use it again later in ``get_context_data()``. If you don't set +``template_name``, the template will default to the normal :class:`ListView` choice, which in this case would be ``"books/book_list.html"`` because it's a list of books; -:class:`ListView` knows nothing about :class:`SingleObjectMixin`, so -it doesn't have any clue this view is anything to do with a Publisher. +:class:`ListView` knows nothing about +:class:`~django.views.generic.detail.SingleObjectMixin`, so it doesn't have +any clue this view is anything to do with a Publisher. .. highlightlang:: html+django @@ -365,7 +372,7 @@ Generally you can use :class:`~django.views.generic.base.TemplateResponseMixin` and :class:`~django.views.generic.detail.SingleObjectMixin` when you need their functionality. As shown above, with a bit of care you can even -combine :class:`SingleObjectMixin` with +combine ``SingleObjectMixin`` with :class:`~django.views.generic.list.ListView`. However things get increasingly complex as you try to do so, and a good rule of thumb is: @@ -376,48 +383,48 @@ increasingly complex as you try to do so, and a good rule of thumb is: list`, :doc:`editing` and date. For example it's fine to combine :class:`TemplateView` (built in view) with - :class:`MultipleObjectMixin` (generic list), but you're likely to - have problems combining :class:`SingleObjectMixin` (generic - detail) with :class:`MultipleObjectMixin` (generic list). + :class:`~django.views.generic.list.MultipleObjectMixin` (generic list), but + you're likely to have problems combining ``SingleObjectMixin`` (generic + detail) with ``MultipleObjectMixin`` (generic list). To show what happens when you try to get more sophisticated, we show an example that sacrifices readability and maintainability when there is a simpler solution. First, let's look at a naive attempt to combine :class:`~django.views.generic.detail.DetailView` with :class:`~django.views.generic.edit.FormMixin` to enable use to -``POST`` a Django :class:`Form` to the same URL as we're displaying an -object using :class:`DetailView`. +``POST`` a Django :class:`~django.forms.Form` to the same URL as we're +displaying an object using :class:`DetailView`. Using FormMixin with DetailView ------------------------------- Think back to our earlier example of using :class:`View` and -:class:`SingleObjectMixin` together. We were recording a user's -interest in a particular author; say now that we want to let them -leave a message saying why they like them. Again, let's assume we're +:class:`~django.views.generic.detail.SingleObjectMixin` together. We were +recording a user's interest in a particular author; say now that we want to +let them leave a message saying why they like them. Again, let's assume we're not going to store this in a relational database but instead in something more esoteric that we won't worry about here. -At this point it's natural to reach for a :class:`Form` to encapsulate -the information sent from the user's browser to Django. Say also that -we're heavily invested in `REST`_, so we want to use the same URL for +At this point it's natural to reach for a :class:`~django.forms.Form` to +encapsulate the information sent from the user's browser to Django. Say also +that we're heavily invested in `REST`_, so we want to use the same URL for displaying the author as for capturing the message from the user. Let's rewrite our ``AuthorDetailView`` to do that. .. _REST: http://en.wikipedia.org/wiki/Representational_state_transfer We'll keep the ``GET`` handling from :class:`DetailView`, although -we'll have to add a :class:`Form` into the context data so we can +we'll have to add a :class:`~django.forms.Form` into the context data so we can render it in the template. We'll also want to pull in form processing from :class:`~django.views.generic.edit.FormMixin`, and write a bit of code so that on ``POST`` the form gets called appropriately. .. note:: - We use :class:`FormMixin` and implement :meth:`post()` ourselves - rather than try to mix :class:`DetailView` with :class:`FormView` - (which provides a suitable :meth:`post()` already) because both of - the views implement :meth:`get()`, and things would get much more + We use :class:`~django.views.generic.edit.FormMixin` and implement + ``post()`` ourselves rather than try to mix :class:`DetailView` with + :class:`FormView` (which provides a suitable ``post()`` already) because + both of the views implement ``get()``, and things would get much more confusing. .. highlightlang:: python @@ -472,24 +479,24 @@ Our new ``AuthorDetail`` looks like this:: # record the interest using the message in form.cleaned_data return super(AuthorDetail, self).form_valid(form) -:meth:`get_success_url()` is just providing somewhere to redirect to, +``get_success_url()`` is just providing somewhere to redirect to, which gets used in the default implementation of -:meth:`form_valid()`. We have to provide our own :meth:`post()` as -noted earlier, and override :meth:`get_context_data()` to make the -:class:`Form` available in the context data. +``form_valid()``. We have to provide our own ``post()`` as +noted earlier, and override ``get_context_data()`` to make the +:class:`~django.forms.Form` available in the context data. A better solution ----------------- It should be obvious that the number of subtle interactions between -:class:`FormMixin` and :class:`DetailView` is already testing our -ability to manage things. It's unlikely you'd want to write this kind -of class yourself. +:class:`~django.views.generic.edit.FormMixin` and :class:`DetailView` is +already testing our ability to manage things. It's unlikely you'd want to +write this kind of class yourself. -In this case, it would be fairly easy to just write the :meth:`post()` +In this case, it would be fairly easy to just write the ``post()`` method yourself, keeping :class:`DetailView` as the only generic -functionality, although writing :class:`Form` handling code involves a -lot of duplication. +functionality, although writing :class:`~django.forms.Form` handling code +involves a lot of duplication. Alternatively, it would still be easier than the above approach to have a separate view for processing the form, which could use @@ -502,15 +509,15 @@ An alternative better solution What we're really trying to do here is to use two different class based views from the same URL. So why not do just that? We have a very clear division here: ``GET`` requests should get the -:class:`DetailView` (with the :class:`Form` added to the context +:class:`DetailView` (with the :class:`~django.forms.Form` added to the context data), and ``POST`` requests should get the :class:`FormView`. Let's set up those views first. The ``AuthorDisplay`` view is almost the same as :ref:`when we first introduced AuthorDetail`; we have to -write our own :meth:`get_context_data()` to make the +write our own ``get_context_data()`` to make the ``AuthorInterestForm`` available to the template. We'll skip the -:meth:`get_object()` override from before for clarity. +``get_object()`` override from before for clarity. .. code-block:: python @@ -533,9 +540,9 @@ write our own :meth:`get_context_data()` to make the return super(AuthorDisplay, self).get_context_data(**context) Then the ``AuthorInterest`` is a simple :class:`FormView`, but we -have to bring in :class:`SingleObjectMixin` so we can find the author -we're talking about, and we have to remember to set -:attr:`template_name` to ensure that form errors will render the same +have to bring in :class:`~django.views.generic.detail.SingleObjectMixin` so we +can find the author we're talking about, and we have to remember to set +``template_name`` to ensure that form errors will render the same template as ``AuthorDisplay`` is using on ``GET``. .. code-block:: python @@ -568,14 +575,14 @@ template as ``AuthorDisplay`` is using on ``GET``. return super(AuthorInterest, self).form_valid(form) Finally we bring this together in a new ``AuthorDetail`` view. We -already know that calling :meth:`as_view()` on a class-based view -gives us something that behaves exactly like a function based view, so -we can do that at the point we choose between the two subviews. +already know that calling :meth:`~django.views.generic.base.View.as_view()` on +a class-based view gives us something that behaves exactly like a function +based view, so we can do that at the point we choose between the two subviews. -You can of course pass through keyword arguments to :meth:`as_view()` -in the same way you would in your URLconf, such as if you wanted the -``AuthorInterest`` behaviour to also appear at another URL but -using a different template. +You can of course pass through keyword arguments to +:meth:`~django.views.generic.base.View.as_view()` in the same way you +would in your URLconf, such as if you wanted the ``AuthorInterest`` behavior +to also appear at another URL but using a different template. .. code-block:: python @@ -646,8 +653,8 @@ Now we mix this into the base TemplateView:: Equally we could use our mixin with one of the generic views. We can make our own version of :class:`~django.views.generic.detail.DetailView` by mixing -:class:`JSONResponseMixin` with the -:class:`~django.views.generic.detail.BaseDetailView` -- (the +``JSONResponseMixin`` with the +``django.views.generic.detail.BaseDetailView`` -- (the :class:`~django.views.generic.detail.DetailView` before template rendering behavior has been mixed in):: @@ -662,11 +669,12 @@ If you want to be really adventurous, you could even mix a :class:`~django.views.generic.detail.DetailView` subclass that is able to return *both* HTML and JSON content, depending on some property of the HTTP request, such as a query argument or a HTTP header. Just mix -in both the :class:`JSONResponseMixin` and a +in both the ``JSONResponseMixin`` and a :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`, -and override the implementation of :func:`render_to_response()` to defer -to the appropriate subclass depending on the type of response that the user -requested:: +and override the implementation of +:func:`~django.views.generic.base.TemplateResponseMixin.render_to_response()` +to defer to the appropriate subclass depending on the type of response that the +user requested:: class HybridDetailView(JSONResponseMixin, SingleObjectTemplateResponseMixin, BaseDetailView): def render_to_response(self, context): @@ -678,5 +686,5 @@ requested:: Because of the way that Python resolves method overloading, the local ``render_to_response()`` implementation will override the versions provided by -:class:`JSONResponseMixin` and +``JSONResponseMixin`` and :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`. diff --git a/docs/topics/db/sql.txt b/docs/topics/db/sql.txt index 310dcb5ae6..6cc174a248 100644 --- a/docs/topics/db/sql.txt +++ b/docs/topics/db/sql.txt @@ -24,9 +24,8 @@ return model instances: .. method:: Manager.raw(raw_query, params=None, translations=None) This method method takes a raw SQL query, executes it, and returns a -:class:`~django.db.models.query.RawQuerySet` instance. This -:class:`~django.db.models.query.RawQuerySet` instance can be iterated -over just like an normal QuerySet to provide object instances. +``django.db.models.query.RawQuerySet`` instance. This ``RawQuerySet`` instance +can be iterated over just like an normal QuerySet to provide object instances. This is best illustrated with an example. Suppose you've got the following model:: diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 7716c91681..11755ff5c5 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -48,10 +48,9 @@ you use the session middleware after the transaction middleware, session creation will be part of the transaction. The various cache middlewares are an exception: -:class:`~django.middleware.cache.CacheMiddleware`, -:class:`~django.middleware.cache.UpdateCacheMiddleware`, and -:class:`~django.middleware.cache.FetchFromCacheMiddleware` are never affected. -Even when using database caching, Django's cache backend uses its own +``CacheMiddleware``, :class:`~django.middleware.cache.UpdateCacheMiddleware`, +and :class:`~django.middleware.cache.FetchFromCacheMiddleware` are never +affected. Even when using database caching, Django's cache backend uses its own database cursor (which is mapped to its own database connection internally). .. note:: diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index b5b02581cd..ee1c69e031 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -3,6 +3,8 @@ Formsets ======== +.. class:: django.forms.formset.BaseFormSet + A formset is a layer of abstraction to working with multiple forms on the same page. It can be best compared to a data grid. Let's say you have the following form:: diff --git a/docs/topics/http/file-uploads.txt b/docs/topics/http/file-uploads.txt index b3a830c25e..53499359e3 100644 --- a/docs/topics/http/file-uploads.txt +++ b/docs/topics/http/file-uploads.txt @@ -227,8 +227,8 @@ field in the model:: ``UploadedFile`` objects ======================== -In addition to those inherited from :class:`File`, all ``UploadedFile`` objects -define the following methods/attributes: +In addition to those inherited from :class:`~django.core.files.File`, all +``UploadedFile`` objects define the following methods/attributes: .. attribute:: UploadedFile.content_type diff --git a/docs/topics/http/views.txt b/docs/topics/http/views.txt index 9ef521c71d..f73ec4f5be 100644 --- a/docs/topics/http/views.txt +++ b/docs/topics/http/views.txt @@ -132,6 +132,8 @@ Customizing error views The 404 (page not found) view ----------------------------- +.. function:: django.views.defaults.page_not_found(request, template_name='404.html') + When you raise an ``Http404`` exception, Django loads a special view devoted to handling 404 errors. By default, it's the view ``django.views.defaults.page_not_found``, which either produces a very simple diff --git a/docs/topics/i18n/timezones.txt b/docs/topics/i18n/timezones.txt index 14c81e6665..22a0edb073 100644 --- a/docs/topics/i18n/timezones.txt +++ b/docs/topics/i18n/timezones.txt @@ -310,7 +310,7 @@ time zone is unset, the default time zone applies. get_current_timezone ~~~~~~~~~~~~~~~~~~~~ -When the :func:`django.core.context_processors.tz` context processor is +When the ``django.core.context_processors.tz`` context processor is enabled -- by default, it is -- each :class:`~django.template.RequestContext` contains a ``TIME_ZONE`` variable that provides the name of the current time zone. @@ -659,7 +659,7 @@ Usage datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=) Note that ``localize`` is a pytz extension to the :class:`~datetime.tzinfo` - API. Also, you may want to catch :exc:`~pytz.InvalidTimeError`. The + API. Also, you may want to catch ``pytz.InvalidTimeError``. The documentation of pytz contains `more examples`_. You should review it before attempting to manipulate aware datetimes. diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index db0c0b3d25..3b68914c1a 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -141,7 +141,7 @@ error log record will be written. Naming loggers -------------- -The call to :meth:`logging.getLogger()` obtains (creating, if +The call to :func:`logging.getLogger()` obtains (creating, if necessary) an instance of a logger. The logger instance is identified by a name. This name is used to identify the logger for configuration purposes. @@ -242,7 +242,7 @@ An example The full documentation for `dictConfig format`_ is the best source of information about logging configuration dictionaries. However, to give you a taste of what is possible, here is an example of a fairly -complex logging setup, configured using :meth:`logging.dictConfig`:: +complex logging setup, configured using :func:`logging.config.dictConfig`:: LOGGING = { 'version': 1, @@ -317,12 +317,12 @@ This logging configuration does the following things: message, plus the time, process, thread and module that generate the log message. -* Defines one filter -- :class:`project.logging.SpecialFilter`, +* Defines one filter -- ``project.logging.SpecialFilter``, using the alias ``special``. If this filter required additional arguments at time of construction, they can be provided as additional keys in the filter configuration dictionary. In this case, the argument ``foo`` will be given a value of ``bar`` when - instantiating the :class:`SpecialFilter`. + instantiating the ``SpecialFilter``. * Defines three handlers: @@ -365,7 +365,7 @@ logger, you can specify your own configuration scheme. The :setting:`LOGGING_CONFIG` setting defines the callable that will be used to configure Django's loggers. By default, it points at -Python's :meth:`logging.dictConfig()` method. However, if you want to +Python's :func:`logging.config.dictConfig()` function. However, if you want to use a different configuration process, you can use any other callable that takes a single argument. The contents of :setting:`LOGGING` will be provided as the value of that argument when logging is configured. @@ -509,7 +509,7 @@ logging module. through the filter. Handling of that record will not proceed if the callback returns False. - For instance, to filter out :class:`~django.http.UnreadablePostError` + For instance, to filter out :exc:`~django.http.UnreadablePostError` (raised when a user cancels an upload) from the admin emails, you would create a filter function:: diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index e1d78a10e6..b44c180d7f 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -78,8 +78,8 @@ wherever possible and avoid the ``b`` prefixes. String handling --------------- -Python 2's :class:`unicode` type was renamed :class:`str` in Python 3, -:class:`str` was renamed :class:`bytes`, and :class:`basestring` disappeared. +Python 2's :func:`unicode` type was renamed :func:`str` in Python 3, +:func:`str` was renamed ``bytes()``, and :func:`basestring` disappeared. six_ provides :ref:`tools ` to deal with these changes. @@ -131,35 +131,36 @@ and ``SafeText`` respectively. For forwards compatibility, the new names work as of Django 1.4.2. -:meth:`__str__` and :meth:`__unicode__` methods ------------------------------------------------ +:meth:`~object.__str__` and :meth:`~object.__unicode__` methods +--------------------------------------------------------------- -In Python 2, the object model specifies :meth:`__str__` and -:meth:`__unicode__` methods. If these methods exist, they must return -:class:`str` (bytes) and :class:`unicode` (text) respectively. +In Python 2, the object model specifies :meth:`~object.__str__` and +:meth:`~object.__unicode__` methods. If these methods exist, they must return +``str`` (bytes) and ``unicode`` (text) respectively. -The ``print`` statement and the :func:`str` built-in call :meth:`__str__` to -determine the human-readable representation of an object. The :func:`unicode` -built-in calls :meth:`__unicode__` if it exists, and otherwise falls back to -:meth:`__str__` and decodes the result with the system encoding. Conversely, -the :class:`~django.db.models.Model` base class automatically derives -:meth:`__str__` from :meth:`__unicode__` by encoding to UTF-8. +The ``print`` statement and the :func:`str` built-in call +:meth:`~object.__str__` to determine the human-readable representation of an +object. The :func:`unicode` built-in calls :meth:`~object.__unicode__` if it +exists, and otherwise falls back to :meth:`~object.__str__` and decodes the +result with the system encoding. Conversely, the +:class:`~django.db.models.Model` base class automatically derives +:meth:`~object.__str__` from :meth:`~object.__unicode__` by encoding to UTF-8. -In Python 3, there's simply :meth:`__str__`, which must return :class:`str` +In Python 3, there's simply :meth:`~object.__str__`, which must return ``str`` (text). -(It is also possible to define :meth:`__bytes__`, but Django application have +(It is also possible to define ``__bytes__()``, but Django application have little use for that method, because they hardly ever deal with -:class:`bytes`.) +``bytes``.) -Django provides a simple way to define :meth:`__str__` and :meth:`__unicode__` -methods that work on Python 2 and 3: you must define a :meth:`__str__` method -returning text and to apply the +Django provides a simple way to define :meth:`~object.__str__` and +:meth:`~object.__unicode__` methods that work on Python 2 and 3: you must +define a :meth:`~object.__str__` method returning text and to apply the :func:`~django.utils.encoding.python_2_unicode_compatible` decorator. On Python 3, the decorator is a no-op. On Python 2, it defines appropriate -:meth:`__unicode__` and :meth:`__str__` methods (replacing the original -:meth:`__str__` method in the process). Here's an example:: +:meth:`~object.__unicode__` and :meth:`~object.__str__` methods (replacing the +original :meth:`~object.__str__` method in the process). Here's an example:: from __future__ import unicode_literals from django.utils.encoding import python_2_unicode_compatible @@ -173,8 +174,8 @@ This technique is the best match for Django's porting philosophy. For forwards compatibility, this decorator is available as of Django 1.4.2. -Finally, note that :meth:`__repr__` must return a :class:`str` on all versions -of Python. +Finally, note that :meth:`~object.__repr__` must return a ``str`` on all +versions of Python. :class:`dict` and :class:`dict`-like classes -------------------------------------------- @@ -187,19 +188,19 @@ behave likewise in Python 3. six_ provides compatibility functions to work around this change: :func:`~six.iterkeys`, :func:`~six.iteritems`, and :func:`~six.itervalues`. Django's bundled version adds :func:`~django.utils.six.iterlists` for -:class:`~django.utils.datastructures.MultiValueDict` and its subclasses. +``django.utils.datastructures.MultiValueDict`` and its subclasses. :class:`~django.http.HttpRequest` and :class:`~django.http.HttpResponse` objects -------------------------------------------------------------------------------- According to :pep:`3333`: -- headers are always :class:`str` objects, -- input and output streams are always :class:`bytes` objects. +- headers are always ``str`` objects, +- input and output streams are always ``bytes`` objects. Specifically, :attr:`HttpResponse.content ` -contains :class:`bytes`, which may become an issue if you compare it with a -:class:`str` in your tests. The preferred solution is to rely on +contains ``bytes``, which may become an issue if you compare it with a +``str`` in your tests. The preferred solution is to rely on :meth:`~django.test.TestCase.assertContains` and :meth:`~django.test.TestCase.assertNotContains`. These methods accept a response and a unicode string as arguments. @@ -236,11 +237,10 @@ under Python 3, use the :func:`str` builtin:: str('my string') -In Python 3, there aren't any automatic conversions between :class:`str` and -:class:`bytes`, and the :mod:`codecs` module became more strict. -:meth:`str.decode` always returns :class:`bytes`, and :meth:`bytes.decode` -always returns :class:`str`. As a consequence, the following pattern is -sometimes necessary:: +In Python 3, there aren't any automatic conversions between ``str`` and +``bytes``, and the :mod:`codecs` module became more strict. :meth:`str.decode` +always returns ``bytes``, and ``bytes.decode`` always returns ``str``. As a +consequence, the following pattern is sometimes necessary:: value = value.encode('ascii', 'ignore').decode('ascii') @@ -395,11 +395,8 @@ The version of six bundled with Django includes one extra function: .. function:: iterlists(MultiValueDict) - Returns an iterator over the lists of values of a - :class:`~django.utils.datastructures.MultiValueDict`. This replaces - :meth:`~django.utils.datastructures.MultiValueDict.iterlists()` on Python - 2 and :meth:`~django.utils.datastructures.MultiValueDict.lists()` on - Python 3. + Returns an iterator over the lists of values of a ``MultiValueDict``. This + replaces ``iterlists()`` on Python 2 and ``lists()`` on Python 3. .. function:: assertRaisesRegex(testcase, *args, **kwargs) diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index e36c7587d1..2af0584a61 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -26,6 +26,8 @@ to (see `Serialization formats`_) and a argument can be any iterator that yields Django model instances, but it'll almost always be a QuerySet). +.. function:: django.core.serializers.get_serializer(format) + You can also use a serializer object directly:: XMLSerializer = serializers.get_serializer("xml") @@ -43,7 +45,7 @@ This is useful if you want to serialize data directly to a file-like object Calling :func:`~django.core.serializers.get_serializer` with an unknown :ref:`format ` will raise a - :class:`~django.core.serializers.SerializerDoesNotExist` exception. + ``django.core.serializers.SerializerDoesNotExist`` exception. Subset of fields ~~~~~~~~~~~~~~~~ diff --git a/docs/topics/settings.txt b/docs/topics/settings.txt index 88fa7b6864..fa26297988 100644 --- a/docs/topics/settings.txt +++ b/docs/topics/settings.txt @@ -32,6 +32,8 @@ Because a settings file is a Python module, the following apply: Designating the settings ======================== +.. envvar:: DJANGO_SETTINGS_MODULE + When you use Django, you have to tell it which settings you're using. Do this by using an environment variable, ``DJANGO_SETTINGS_MODULE``. @@ -260,4 +262,3 @@ It boils down to this: Use exactly one of either ``configure()`` or ``DJANGO_SETTINGS_MODULE``. Not both, and not neither. .. _@login_required: ../authentication/#the-login-required-decorator - diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index e51741e549..534569efeb 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -28,7 +28,7 @@ module defines tests in class-based approach. backported for Python 2.5 compatibility. To access this library, Django provides the - :mod:`django.utils.unittest` module alias. If you are using Python + ``django.utils.unittest`` module alias. If you are using Python 2.7, or you have installed unittest2 locally, Django will map the alias to the installed version of the unittest library. Otherwise, Django will use its own bundled version of unittest2. @@ -853,7 +853,7 @@ Normal Python unit test classes extend a base class of Hierarchy of Django unit testing classes Regardless of the version of Python you're using, if you've installed -``unittest2``, :mod:`django.utils.unittest` will point to that library. +``unittest2``, ``django.utils.unittest`` will point to that library. SimpleTestCase ~~~~~~~~~~~~~~ @@ -882,7 +882,7 @@ features like: then you should use :class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase` instead. -``SimpleTestCase`` inherits from :class:`django.utils.unittest.TestCase`. +``SimpleTestCase`` inherits from ``django.utils.unittest.TestCase``. TransactionTestCase ~~~~~~~~~~~~~~~~~~~ @@ -1724,7 +1724,7 @@ test if the database doesn't support a specific named feature. The decorators use a string identifier to describe database features. This string corresponds to attributes of the database connection -features class. See :class:`~django.db.backends.BaseDatabaseFeatures` +features class. See ``django.db.backends.BaseDatabaseFeatures`` class for a full list of database features that can be used as a basis for skipping tests. -- cgit v1.3 From 50a985b09b439a0d52aad8694d377a3483cb02e1 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Tue, 1 Jan 2013 22:28:48 +0100 Subject: Fixed #19099 -- Split broken link emails out of common middleware. --- django/conf/global_settings.py | 4 +- django/middleware/common.py | 78 ++++++++++++++++++------------- docs/howto/error-reporting.txt | 19 ++++---- docs/internals/deprecation.txt | 7 +++ docs/ref/middleware.txt | 8 ++-- docs/ref/settings.txt | 13 ++++-- docs/releases/1.6.txt | 18 +++++++ tests/regressiontests/middleware/tests.py | 61 ++++++++++++++++++------ 8 files changed, 147 insertions(+), 61 deletions(-) (limited to 'docs/internals') diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 4d69c6365f..740c792dcf 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -146,7 +146,7 @@ FILE_CHARSET = 'utf-8' # Email address that error messages come from. SERVER_EMAIL = 'root@localhost' -# Whether to send broken-link emails. +# Whether to send broken-link emails. Deprecated, must be removed in 1.8. SEND_BROKEN_LINK_EMAILS = False # Database connection info. If left empty, will default to the dummy backend. @@ -245,7 +245,7 @@ ALLOWED_INCLUDE_ROOTS = () ADMIN_FOR = () # List of compiled regular expression objects representing URLs that need not -# be reported when SEND_BROKEN_LINK_EMAILS is True. Here are a few examples: +# be reported by BrokenLinkEmailsMiddleware. Here are a few examples: # import re # IGNORABLE_404_URLS = ( # re.compile(r'^/apple-touch-icon.*\.png$'), diff --git a/django/middleware/common.py b/django/middleware/common.py index c6e71e0d48..92f8cb3992 100644 --- a/django/middleware/common.py +++ b/django/middleware/common.py @@ -1,13 +1,14 @@ import hashlib import logging import re +import warnings from django.conf import settings -from django import http from django.core.mail import mail_managers +from django.core import urlresolvers +from django import http from django.utils.http import urlquote from django.utils import six -from django.core import urlresolvers logger = logging.getLogger('django.request') @@ -102,25 +103,15 @@ class CommonMiddleware(object): return http.HttpResponsePermanentRedirect(newurl) def process_response(self, request, response): - "Send broken link emails and calculate the Etag, if needed." - if response.status_code == 404: - if settings.SEND_BROKEN_LINK_EMAILS and not settings.DEBUG: - # If the referrer was from an internal link or a non-search-engine site, - # send a note to the managers. - domain = request.get_host() - referer = request.META.get('HTTP_REFERER', None) - is_internal = _is_internal_request(domain, referer) - path = request.get_full_path() - if referer and not _is_ignorable_404(path) and (is_internal or '?' not in referer): - ua = request.META.get('HTTP_USER_AGENT', '') - ip = request.META.get('REMOTE_ADDR', '') - mail_managers("Broken %slink on %s" % ((is_internal and 'INTERNAL ' or ''), domain), - "Referrer: %s\nRequested URL: %s\nUser agent: %s\nIP address: %s\n" \ - % (referer, request.get_full_path(), ua, ip), - fail_silently=True) - return response - - # Use ETags, if requested. + """ + Calculate the ETag, if needed. + """ + if settings.SEND_BROKEN_LINK_EMAILS: + warnings.warn("SEND_BROKEN_LINK_EMAILS is deprecated. " + "Use BrokenLinkEmailsMiddleware instead.", + PendingDeprecationWarning, stacklevel=2) + BrokenLinkEmailsMiddleware().process_response(request, response) + if settings.USE_ETAGS: if response.has_header('ETag'): etag = response['ETag'] @@ -139,15 +130,38 @@ class CommonMiddleware(object): return response -def _is_ignorable_404(uri): - """ - Returns True if a 404 at the given URL *shouldn't* notify the site managers. - """ - return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS) -def _is_internal_request(domain, referer): - """ - Returns true if the referring URL is the same domain as the current request. - """ - # Different subdomains are treated as different domains. - return referer is not None and re.match("^https?://%s/" % re.escape(domain), referer) +class BrokenLinkEmailsMiddleware(object): + + def process_response(self, request, response): + """ + Send broken link emails for relevant 404 NOT FOUND responses. + """ + if response.status_code == 404 and not settings.DEBUG: + domain = request.get_host() + path = request.get_full_path() + referer = request.META.get('HTTP_REFERER', '') + is_internal = self.is_internal_request(domain, referer) + is_not_search_engine = '?' not in referer + is_ignorable = self.is_ignorable_404(path) + if referer and (is_internal or is_not_search_engine) and not is_ignorable: + ua = request.META.get('HTTP_USER_AGENT', '') + ip = request.META.get('REMOTE_ADDR', '') + mail_managers( + "Broken %slink on %s" % (('INTERNAL ' if is_internal else ''), domain), + "Referrer: %s\nRequested URL: %s\nUser agent: %s\nIP address: %s\n" % (referer, path, ua, ip), + fail_silently=True) + return response + + def is_internal_request(self, domain, referer): + """ + Returns True if the referring URL is the same domain as the current request. + """ + # Different subdomains are treated as different domains. + return re.match("^https?://%s/" % re.escape(domain), referer) + + def is_ignorable_404(self, uri): + """ + Returns True if a 404 at the given URL *shouldn't* notify the site managers. + """ + return any(pattern.search(uri) for pattern in settings.IGNORABLE_404_URLS) diff --git a/docs/howto/error-reporting.txt b/docs/howto/error-reporting.txt index 742b81b7e2..7f3c68c136 100644 --- a/docs/howto/error-reporting.txt +++ b/docs/howto/error-reporting.txt @@ -54,18 +54,24 @@ setting. Django can also be configured to email errors about broken links (404 "page not found" errors). Django sends emails about 404 errors when: -* :setting:`DEBUG` is ``False`` +* :setting:`DEBUG` is ``False``; -* :setting:`SEND_BROKEN_LINK_EMAILS` is ``True`` - -* Your :setting:`MIDDLEWARE_CLASSES` setting includes ``CommonMiddleware`` - (which it does by default). +* Your :setting:`MIDDLEWARE_CLASSES` setting includes + :class:`django.middleware.common.BrokenLinkEmailsMiddleware`. If those conditions are met, Django will email the users listed in the :setting:`MANAGERS` setting whenever your code raises a 404 and the request has a referer. (It doesn't bother to email for 404s that don't have a referer -- those are usually just people typing in broken URLs or broken Web 'bots). +.. note:: + + :class:`~django.middleware.common.BrokenLinkEmailsMiddleware` must appear + before other middleware that intercepts 404 errors, such as + :class:`~django.middleware.locale.LocaleMiddleware` or + :class:`~django.contrib.flatpages.middleware.FlatpageFallbackMiddleware`. + Put it towards the top of your :setting:`MIDDLEWARE_CLASSES` setting. + You can tell Django to stop reporting particular 404s by tweaking the :setting:`IGNORABLE_404_URLS` setting. It should be a tuple of compiled regular expression objects. For example:: @@ -92,9 +98,6 @@ crawlers often request:: (Note that these are regular expressions, so we put a backslash in front of periods to escape them.) -The best way to disable this behavior is to set -:setting:`SEND_BROKEN_LINK_EMAILS` to ``False``. - .. seealso:: 404 errors are logged using the logging framework. By default, these log diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index faa6d1ff02..63d65d1e4a 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -308,6 +308,13 @@ these changes. * The ``depth`` keyword argument will be removed from :meth:`~django.db.models.query.QuerySet.select_related`. +1.8 +--- + +* The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the + :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to + your :setting:`MIDDLEWARE_CLASSES` setting instead. + 2.0 --- diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 2b053d80ab..1e6e57f720 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -61,14 +61,16 @@ Adds a few conveniences for perfectionists: indexer would treat them as separate URLs -- so it's best practice to normalize URLs. -* Sends broken link notification emails to :setting:`MANAGERS` if - :setting:`SEND_BROKEN_LINK_EMAILS` is set to ``True``. - * Handles ETags based on the :setting:`USE_ETAGS` setting. If :setting:`USE_ETAGS` is set to ``True``, Django will calculate an ETag for each request by MD5-hashing the page content, and it'll take care of sending ``Not Modified`` responses, if appropriate. +.. class:: BrokenLinkEmailsMiddleware + +* Sends broken link notification emails to :setting:`MANAGERS` (see + :doc:`/howto/error-reporting`). + View metadata middleware ------------------------ diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 110d5dbdc9..d057323c06 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -1090,8 +1090,9 @@ query string, if any). Use this if your site does not provide a commonly requested file such as ``favicon.ico`` or ``robots.txt``, or if it gets hammered by script kiddies. -This is only used if :setting:`SEND_BROKEN_LINK_EMAILS` is set to ``True`` and -``CommonMiddleware`` is installed (see :doc:`/topics/http/middleware`). +This is only used if +:class:`~django.middleware.common.BrokenLinkEmailsMiddleware` is enabled (see +:doc:`/topics/http/middleware`). .. setting:: INSTALLED_APPS @@ -1250,7 +1251,8 @@ MANAGERS Default: ``()`` (Empty tuple) A tuple in the same format as :setting:`ADMINS` that specifies who should get -broken-link notifications when :setting:`SEND_BROKEN_LINK_EMAILS` is ``True``. +broken link notifications when +:class:`~django.middleware.common.BrokenLinkEmailsMiddleware` is enabled. .. setting:: MEDIA_ROOT @@ -1448,6 +1450,11 @@ available in ``request.META``.) SEND_BROKEN_LINK_EMAILS ----------------------- +.. deprecated:: 1.6 + Since :class:`~django.middleware.common.BrokenLinkEmailsMiddleware` + was split from :class:`~django.middleware.common.CommonMiddleware`, + this setting no longer serves a purpose. + Default: ``False`` Whether to send an email to the :setting:`MANAGERS` each time somebody visits diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index e425036839..dcf6f2604a 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -46,3 +46,21 @@ Backwards incompatible changes in 1.6 Features deprecated in 1.6 ========================== + +``SEND_BROKEN_LINK_EMAILS`` setting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`~django.middleware.common.CommonMiddleware` used to provide basic +reporting of broken links by email when ``SEND_BROKEN_LINK_EMAILS`` is set to +``True``. + +Because of intractable ordering problems between +:class:`~django.middleware.common.CommonMiddleware` and +:class:`~django.middleware.locale.LocaleMiddleware`, this feature was split +out into a new middleware: +:class:`~django.middleware.common.BrokenLinkEmailsMiddleware`. + +If you're relying on this feature, you should add +``'django.middleware.common.BrokenLinkEmailsMiddleware'`` to your +:setting:`MIDDLEWARE_CLASSES` setting and remove ``SEND_BROKEN_LINK_EMAILS`` +from your settings. diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py index e3d8350da6..c6d42a6964 100644 --- a/tests/regressiontests/middleware/tests.py +++ b/tests/regressiontests/middleware/tests.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- import gzip -import re -import random from io import BytesIO +import random +import re +import warnings from django.conf import settings from django.core import mail from django.http import HttpRequest from django.http import HttpResponse, StreamingHttpResponse from django.middleware.clickjacking import XFrameOptionsMiddleware -from django.middleware.common import CommonMiddleware +from django.middleware.common import CommonMiddleware, BrokenLinkEmailsMiddleware from django.middleware.http import ConditionalGetMiddleware from django.middleware.gzip import GZipMiddleware from django.test import TestCase, RequestFactory @@ -232,33 +233,39 @@ class CommonMiddlewareTest(TestCase): self.assertEqual(r['Location'], 'http://www.testserver/middleware/customurlconf/slash/') - # Tests for the 404 error reporting via email + # Legacy tests for the 404 error reporting via email (to be removed in 1.8) @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), - SEND_BROKEN_LINK_EMAILS = True) + SEND_BROKEN_LINK_EMAILS=True) def test_404_error_reporting(self): request = self._get_request('regular_url/that/does/not/exist') request.META['HTTP_REFERER'] = '/another/url/' - response = self.client.get(request.path) - CommonMiddleware().process_response(request, response) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + response = self.client.get(request.path) + CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 1) self.assertIn('Broken', mail.outbox[0].subject) @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), - SEND_BROKEN_LINK_EMAILS = True) + SEND_BROKEN_LINK_EMAILS=True) def test_404_error_reporting_no_referer(self): request = self._get_request('regular_url/that/does/not/exist') - response = self.client.get(request.path) - CommonMiddleware().process_response(request, response) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + response = self.client.get(request.path) + CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 0) @override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),), - SEND_BROKEN_LINK_EMAILS = True) + SEND_BROKEN_LINK_EMAILS=True) def test_404_error_reporting_ignored_url(self): request = self._get_request('foo_url/that/does/not/exist/either') request.META['HTTP_REFERER'] = '/another/url/' - response = self.client.get(request.path) - CommonMiddleware().process_response(request, response) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", PendingDeprecationWarning) + response = self.client.get(request.path) + CommonMiddleware().process_response(request, response) self.assertEqual(len(mail.outbox), 0) # Other tests @@ -271,6 +278,34 @@ class CommonMiddlewareTest(TestCase): self.assertEqual(response.status_code, 301) +@override_settings(IGNORABLE_404_URLS=(re.compile(r'foo'),)) +class BrokenLinkEmailsMiddlewareTest(TestCase): + + def setUp(self): + self.req = HttpRequest() + self.req.META = { + 'SERVER_NAME': 'testserver', + 'SERVER_PORT': 80, + } + self.req.path = self.req.path_info = 'regular_url/that/does/not/exist' + self.resp = self.client.get(self.req.path) + + def test_404_error_reporting(self): + self.req.META['HTTP_REFERER'] = '/another/url/' + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 1) + self.assertIn('Broken', mail.outbox[0].subject) + + def test_404_error_reporting_no_referer(self): + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 0) + + def test_404_error_reporting_ignored_url(self): + self.req.path = self.req.path_info = 'foo_url/that/does/not/exist' + BrokenLinkEmailsMiddleware().process_response(self.req, self.resp) + self.assertEqual(len(mail.outbox), 0) + + class ConditionalGetMiddlewareTest(TestCase): urls = 'regressiontests.middleware.cond_get_urls' def setUp(self): -- cgit v1.3 From eaa716a4130bb019204669d32389db8b399c0f71 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Thu, 24 Jan 2013 06:53:32 -0500 Subject: Fixed #19639 - Updated contributing to reflect model choices best practices. Thanks charettes. --- .../contributing/writing-code/coding-style.txt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/writing-code/coding-style.txt b/docs/internals/contributing/writing-code/coding-style.txt index 0d84cdac9a..21146600b4 100644 --- a/docs/internals/contributing/writing-code/coding-style.txt +++ b/docs/internals/contributing/writing-code/coding-style.txt @@ -136,14 +136,17 @@ Model style * ``def get_absolute_url()`` * Any custom methods -* If ``choices`` is defined for a given model field, define the choices as - a tuple of tuples, with an all-uppercase name, either near the top of - the model module or just above the model class. Example:: - - DIRECTION_CHOICES = ( - ('U', 'Up'), - ('D', 'Down'), - ) +* If ``choices`` is defined for a given model field, define each choice as + a tuple of tuples, with an all-uppercase name as a class attribute on the + model. Example:: + + class MyModel(models.Model): + DIRECTION_UP = 'U' + DIRECTION_DOWN = 'D' + DIRECTION_CHOICES = ( + (DIRECTION_UP, 'Up'), + (DIRECTION_DOWN, 'Down'), + ) Use of ``django.conf.settings`` ------------------------------- -- cgit v1.3 From ee26797cff6ef17817c58e6a2f86db81c21f9800 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Tue, 29 Jan 2013 08:45:40 -0700 Subject: Fixed typos in docs and comments --- django/contrib/gis/gdal/geometries.py | 2 +- django/contrib/gis/geos/prototypes/geom.py | 2 +- django/contrib/gis/utils/srs.py | 2 +- django/contrib/staticfiles/views.py | 2 +- django/db/models/query.py | 2 +- django/middleware/csrf.py | 4 ++-- docs/internals/contributing/committing-code.txt | 2 +- docs/internals/contributing/writing-code/working-with-git.txt | 4 ++-- docs/intro/overview.txt | 4 +++- docs/misc/api-stability.txt | 3 ++- docs/ref/contrib/admin/index.txt | 2 +- docs/ref/contrib/gis/install/index.txt | 2 +- docs/topics/auth/default.txt | 2 +- tests/regressiontests/admin_views/tests.py | 2 +- tests/regressiontests/builtin_server/tests.py | 2 +- 15 files changed, 20 insertions(+), 17 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/gis/gdal/geometries.py b/django/contrib/gis/gdal/geometries.py index eb67059245..0d75620b4e 100644 --- a/django/contrib/gis/gdal/geometries.py +++ b/django/contrib/gis/gdal/geometries.py @@ -76,7 +76,7 @@ class OGRGeometry(GDALBase): str_instance = isinstance(geom_input, six.string_types) - # If HEX, unpack input to to a binary buffer. + # If HEX, unpack input to a binary buffer. if str_instance and hex_regex.match(geom_input): geom_input = memoryview(a2b_hex(geom_input.upper().encode())) str_instance = False diff --git a/django/contrib/gis/geos/prototypes/geom.py b/django/contrib/gis/geos/prototypes/geom.py index 5a614fe5f0..2683c2e25d 100644 --- a/django/contrib/gis/geos/prototypes/geom.py +++ b/django/contrib/gis/geos/prototypes/geom.py @@ -27,7 +27,7 @@ def bin_constructor(func): # HEX & WKB output def bin_output(func): - "Generates a prototype for the routines that return a a sized string." + "Generates a prototype for the routines that return a sized string." func.argtypes = [GEOM_PTR, POINTER(c_size_t)] func.errcheck = check_sized_string func.restype = c_uchar_p diff --git a/django/contrib/gis/utils/srs.py b/django/contrib/gis/utils/srs.py index 07a593d90e..fe2f291cb8 100644 --- a/django/contrib/gis/utils/srs.py +++ b/django/contrib/gis/utils/srs.py @@ -24,7 +24,7 @@ def add_srs_entry(srs, auth_name='EPSG', auth_srid=None, ref_sys_name=None, Defaults to the SRID determined by GDAL. ref_sys_name: - For SpatiaLite users only, sets the value of the the `ref_sys_name` field. + For SpatiaLite users only, sets the value of the `ref_sys_name` field. Defaults to the name determined by GDAL. database: diff --git a/django/contrib/staticfiles/views.py b/django/contrib/staticfiles/views.py index 85459812ad..fe095ed225 100644 --- a/django/contrib/staticfiles/views.py +++ b/django/contrib/staticfiles/views.py @@ -32,7 +32,7 @@ def serve(request, path, document_root=None, insecure=False, **kwargs): """ if not settings.DEBUG and not insecure: raise ImproperlyConfigured("The staticfiles view can only be used in " - "debug mode or if the the --insecure " + "debug mode or if the --insecure " "option of 'runserver' is used") normalized_path = posixpath.normpath(unquote(path)).lstrip('/') absolute_path = finders.find(normalized_path) diff --git a/django/db/models/query.py b/django/db/models/query.py index 68d7931729..eda71d2478 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1587,7 +1587,7 @@ def prefetch_related_objects(result_cache, related_lookups): continue done_lookups.add(lookup) - # Top level, the list of objects to decorate is the the result cache + # Top level, the list of objects to decorate is the result cache # from the primary QuerySet. It won't be for deeper levels. obj_list = result_cache diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py index b2eb0df3f5..339f42a110 100644 --- a/django/middleware/csrf.py +++ b/django/middleware/csrf.py @@ -41,10 +41,10 @@ def _get_new_csrf_key(): def get_token(request): """ - Returns the the CSRF token required for a POST form. The token is an + Returns the CSRF token required for a POST form. The token is an alphanumeric value. - A side effect of calling this function is to make the the csrf_protect + A side effect of calling this function is to make the csrf_protect decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' header to the outgoing response. For this reason, you may need to use this function lazily, as is done by the csrf context processor. diff --git a/docs/internals/contributing/committing-code.txt b/docs/internals/contributing/committing-code.txt index 67dda02f8b..bc2f97a485 100644 --- a/docs/internals/contributing/committing-code.txt +++ b/docs/internals/contributing/committing-code.txt @@ -116,7 +116,7 @@ Practicality beats purity, so it is up to each committer to decide how much history mangling to do for a pull request. The main points are engaging the community, getting work done, and having a usable commit history. -.. _committing-guidlines: +.. _committing-guidelines: Committing guidelines --------------------- diff --git a/docs/internals/contributing/writing-code/working-with-git.txt b/docs/internals/contributing/writing-code/working-with-git.txt index d4a95ae45a..dcfdd9e85b 100644 --- a/docs/internals/contributing/writing-code/working-with-git.txt +++ b/docs/internals/contributing/writing-code/working-with-git.txt @@ -81,7 +81,7 @@ commit them:: git commit When writing the commit message, follow the :ref:`commit message -guidelines ` to ease the work of the committer. If +guidelines ` to ease the work of the committer. If you're uncomfortable with English, try at least to describe precisely what the commit does. @@ -121,7 +121,7 @@ a pull request at GitHub. A good pull request means: * well-formed messages for each commit: a summary line and then paragraphs wrapped at 72 characters thereafter -- see the :ref:`committing guidelines - ` for more details, + ` for more details, * documentation and tests, if needed -- actually tests are always needed, except for documentation changes. diff --git a/docs/intro/overview.txt b/docs/intro/overview.txt index 7cca8bf51b..4f3cd47310 100644 --- a/docs/intro/overview.txt +++ b/docs/intro/overview.txt @@ -56,7 +56,9 @@ Enjoy the free API ================== With that, you've got a free, and rich, :doc:`Python API ` to -access your data. The API is created on the fly, no code generation necessary:: +access your data. The API is created on the fly, no code generation necessary: + +.. code-block:: python # Import the models we created from our "news" app >>> from news.models import Reporter, Article diff --git a/docs/misc/api-stability.txt b/docs/misc/api-stability.txt index 8ae3c716df..3c265be04f 100644 --- a/docs/misc/api-stability.txt +++ b/docs/misc/api-stability.txt @@ -118,7 +118,8 @@ Security fixes If we become aware of a security problem -- hopefully by someone following our :ref:`security reporting policy ` -- we'll do -everything necessary to fix it. This might mean breaking backwards compatibility; security trumps the compatibility guarantee. +everything necessary to fix it. This might mean breaking backwards +compatibility; security trumps the compatibility guarantee. Contributed applications (``django.contrib``) --------------------------------------------- diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index a862d55875..1c19499d2a 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -824,7 +824,7 @@ subclass:: added last after all editable fields. A read-only field can not only display data from a model's field, it can - also display the output of a a model's method or a method of the + also display the output of a model's method or a method of the ``ModelAdmin`` class itself. This is very similar to the way :attr:`ModelAdmin.list_display` behaves. This provides an easy way to use the admin interface to provide feedback on the status of the objects being diff --git a/docs/ref/contrib/gis/install/index.txt b/docs/ref/contrib/gis/install/index.txt index 35c01c9b7e..2987539f33 100644 --- a/docs/ref/contrib/gis/install/index.txt +++ b/docs/ref/contrib/gis/install/index.txt @@ -330,7 +330,7 @@ described above, ``psycopg2`` may be installed using the following command:: .. note:: - If you don't have ``pip``, follow the the :ref:`installation instructions + If you don't have ``pip``, follow the :ref:`installation instructions ` to install it. .. _fink: diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt index 569738569d..1a57770b2b 100644 --- a/docs/topics/auth/default.txt +++ b/docs/topics/auth/default.txt @@ -82,7 +82,7 @@ Changing passwords Django does not store raw (clear text) passwords on the user model, but only a hash (see :doc:`documentation of how passwords are managed ` for full details). Because of this, do not attempt to -manipulate the password attribute of the user directly. This is why a a helper +manipulate the password attribute of the user directly. This is why a helper function is used when creating a user. To change a user's password, you have several options: diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index d7d01e6a92..1633fba6b5 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -2454,7 +2454,7 @@ class TestCustomChangeList(TestCase): self.assertEqual(response.status_code, 302) # redirect somewhere # Hit the page once to get messages out of the queue message list response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) - # Ensure that that data is still not visible on the page + # Ensure that data is still not visible on the page response = self.client.get('/test_admin/%s/admin_views/gadget/' % self.urlbit) self.assertEqual(response.status_code, 200) self.assertNotContains(response, 'First Gadget') diff --git a/tests/regressiontests/builtin_server/tests.py b/tests/regressiontests/builtin_server/tests.py index c8dc77e42e..041bb3c319 100644 --- a/tests/regressiontests/builtin_server/tests.py +++ b/tests/regressiontests/builtin_server/tests.py @@ -7,7 +7,7 @@ from django.utils.unittest import TestCase # # Tests for #9659: wsgi.file_wrapper in the builtin server. -# We need to mock a couple of of handlers and keep track of what +# We need to mock a couple of handlers and keep track of what # gets called when using a couple kinds of WSGI apps. # -- cgit v1.3 From 89cb771be7b53c40642872cdbedb15943bdf8e34 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 31 Jan 2013 13:39:29 +0100 Subject: Fixed #19692 -- Completed deprecation of mimetype in favor of content_type. Thanks Tim for the report and initial patch. --- django/contrib/sitemaps/views.py | 26 +++++++++++++++---- django/shortcuts/__init__.py | 10 ++++++- docs/howto/outputting-csv.txt | 6 ++--- docs/howto/outputting-pdf.txt | 4 +-- docs/internals/deprecation.txt | 10 +++++-- docs/ref/contrib/admin/actions.txt | 4 +-- docs/ref/template-response.txt | 36 +++++++++++++++----------- docs/topics/http/shortcuts.txt | 13 +++++++--- tests/regressiontests/views/generic_urls.py | 2 +- tests/regressiontests/views/tests/shortcuts.py | 4 +-- tests/regressiontests/views/views.py | 6 ++--- 11 files changed, 81 insertions(+), 40 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/sitemaps/views.py b/django/contrib/sitemaps/views.py index cfe3aa66a9..c8d2f4dfa0 100644 --- a/django/contrib/sitemaps/views.py +++ b/django/contrib/sitemaps/views.py @@ -1,3 +1,5 @@ +import warnings + from django.contrib.sites.models import get_current_site from django.core import urlresolvers from django.core.paginator import EmptyPage, PageNotAnInteger @@ -6,8 +8,15 @@ from django.template.response import TemplateResponse from django.utils import six def index(request, sitemaps, - template_name='sitemap_index.xml', mimetype='application/xml', - sitemap_url_name='django.contrib.sitemaps.views.sitemap'): + template_name='sitemap_index.xml', content_type='application/xml', + sitemap_url_name='django.contrib.sitemaps.views.sitemap', + mimetype=None): + + if mimetype: + warnings.warn("The mimetype keyword argument is deprecated, use " + "content_type instead", DeprecationWarning, stacklevel=2) + content_type = mimetype + req_protocol = 'https' if request.is_secure() else 'http' req_site = get_current_site(request) @@ -24,10 +33,17 @@ def index(request, sitemaps, sites.append('%s?p=%s' % (absolute_url, page)) return TemplateResponse(request, template_name, {'sitemaps': sites}, - content_type=mimetype) + content_type=content_type) def sitemap(request, sitemaps, section=None, - template_name='sitemap.xml', mimetype='application/xml'): + template_name='sitemap.xml', content_type='application/xml', + mimetype=None): + + if mimetype: + warnings.warn("The mimetype keyword argument is deprecated, use " + "content_type instead", DeprecationWarning, stacklevel=2) + content_type = mimetype + req_protocol = 'https' if request.is_secure() else 'http' req_site = get_current_site(request) @@ -51,4 +67,4 @@ def sitemap(request, sitemaps, section=None, except PageNotAnInteger: raise Http404("No page '%s'" % page) return TemplateResponse(request, template_name, {'urlset': urls}, - content_type=mimetype) + content_type=content_type) diff --git a/django/shortcuts/__init__.py b/django/shortcuts/__init__.py index 9f896347a4..21bd7a06d2 100644 --- a/django/shortcuts/__init__.py +++ b/django/shortcuts/__init__.py @@ -3,6 +3,7 @@ This module collects helper functions and classes that "span" multiple levels of MVC. In other words, these functions/classes introduce controlled coupling for convenience's sake. """ +import warnings from django.template import loader, RequestContext from django.http import HttpResponse, Http404 @@ -17,7 +18,14 @@ def render_to_response(*args, **kwargs): Returns a HttpResponse whose content is filled with the result of calling django.template.loader.render_to_string() with the passed arguments. """ - httpresponse_kwargs = {'content_type': kwargs.pop('mimetype', None)} + httpresponse_kwargs = {'content_type': kwargs.pop('content_type', None)} + + mimetype = kwargs.pop('mimetype', None) + if mimetype: + warnings.warn("The mimetype keyword argument is deprecated, use " + "content_type instead", DeprecationWarning, stacklevel=2) + httpresponse_kwargs['content_type'] = mimetype + return HttpResponse(loader.render_to_string(*args, **kwargs), **httpresponse_kwargs) def render(request, *args, **kwargs): diff --git a/docs/howto/outputting-csv.txt b/docs/howto/outputting-csv.txt index bcc6f3827b..1f9efb5a4b 100644 --- a/docs/howto/outputting-csv.txt +++ b/docs/howto/outputting-csv.txt @@ -20,7 +20,7 @@ Here's an example:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. - response = HttpResponse(mimetype='text/csv') + response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' writer = csv.writer(response) @@ -92,7 +92,7 @@ Here's an example, which generates the same CSV file as above:: def some_view(request): # Create the HttpResponse object with the appropriate CSV header. - response = HttpResponse(mimetype='text/csv') + response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="somefilename.csv"' # The data is hard-coded here, but you could load it from a database or @@ -111,7 +111,7 @@ Here's an example, which generates the same CSV file as above:: The only difference between this example and the previous example is that this one uses template loading instead of the CSV module. The rest of the code -- -such as the ``mimetype='text/csv'`` -- is the same. +such as the ``content_type='text/csv'`` -- is the same. Then, create the template ``my_template_name.txt``, with this template code: diff --git a/docs/howto/outputting-pdf.txt b/docs/howto/outputting-pdf.txt index 9d87b97710..d15f94f7f4 100644 --- a/docs/howto/outputting-pdf.txt +++ b/docs/howto/outputting-pdf.txt @@ -51,7 +51,7 @@ Here's a "Hello World" example:: def some_view(request): # Create the HttpResponse object with the appropriate PDF headers. - response = HttpResponse(mimetype='application/pdf') + response = HttpResponse(content_type='application/pdf') response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"' # Create the PDF object, using the response object as its "file." @@ -120,7 +120,7 @@ Here's the above "Hello World" example rewritten to use :mod:`io`:: def some_view(request): # Create the HttpResponse object with the appropriate PDF headers. - response = HttpResponse(mimetype='application/pdf') + response = HttpResponse(content_type='application/pdf') response['Content-Disposition'] = 'attachment; filename="somefilename.pdf"' buffer = BytesIO() diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 63d65d1e4a..da0d1e212c 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -290,8 +290,14 @@ these changes. specified as a plain string instead of a tuple will be removed and raise an exception. -* The ``mimetype`` argument to :class:`~django.http.HttpResponse` ``__init__`` - will be removed (``content_type`` should be used instead). +* The ``mimetype`` argument to the ``__init__`` methods of + :class:`~django.http.HttpResponse`, + :class:`~django.template.response.SimpleTemplateResponse`, and + :class:`~django.template.response.TemplateResponse`, will be removed. + ``content_type`` should be used instead. This also applies to the + :func:`~django.shortcuts.render_to_response` shortcut and + the sitemamp views, :func:`~django.contrib.sitemaps.views.index` and + :func:`~django.contrib.sitemaps.views.sitemap`. * When :class:`~django.http.HttpResponse` is instantiated with an iterator, or when :attr:`~django.http.HttpResponse.content` is set to an iterator, diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index d7eef623d5..0a302ecd1d 100644 --- a/docs/ref/contrib/admin/actions.txt +++ b/docs/ref/contrib/admin/actions.txt @@ -223,7 +223,7 @@ objects as JSON:: from django.core import serializers def export_as_json(modeladmin, request, queryset): - response = HttpResponse(mimetype="text/javascript") + response = HttpResponse(content_type="application/json") serializers.serialize("json", queryset, stream=response) return response @@ -356,5 +356,3 @@ Conditionally enabling or disabling actions if 'delete_selected' in actions: del actions['delete_selected'] return actions - - diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index 3f5e772737..844b5fa46b 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -56,11 +56,11 @@ Attributes Methods ------- -.. method:: SimpleTemplateResponse.__init__(template, context=None, mimetype=None, status=None, content_type=None) +.. method:: SimpleTemplateResponse.__init__(template, context=None, content_type=None, status=None) Instantiates a :class:`~django.template.response.SimpleTemplateResponse` object - with the given template, context, MIME type and HTTP status. + with the given template, context, content type, and HTTP status. ``template`` The full name of a template, or a sequence of template names. @@ -75,12 +75,15 @@ Methods The HTTP Status code for the response. ``content_type`` - An alias for ``mimetype``. Historically, this parameter was only called - ``mimetype``, but since this is actually the value included in the HTTP - ``Content-Type`` header, it can also include the character set encoding, - which makes it more than just a MIME type specification. If ``mimetype`` - is specified (not ``None``), that value is used. Otherwise, - ``content_type`` is used. If neither is given, + + .. versionchanged:: 1.5 + + Historically, this parameter was only called ``mimetype`` (now + deprecated), but since this is actually the value included in the HTTP + ``Content-Type`` header, it can also include the character set + encoding, which makes it more than just a MIME type specification. If + ``mimetype`` is specified (not ``None``), that value is used. + Otherwise, ``content_type`` is used. If neither is given, :setting:`DEFAULT_CONTENT_TYPE` is used. @@ -144,7 +147,7 @@ TemplateResponse objects Methods ------- -.. method:: TemplateResponse.__init__(request, template, context=None, mimetype=None, status=None, content_type=None, current_app=None) +.. method:: TemplateResponse.__init__(request, template, context=None, content_type=None, status=None, current_app=None) Instantiates an ``TemplateResponse`` object with the given template, context, MIME type and HTTP status. @@ -165,12 +168,15 @@ Methods The HTTP Status code for the response. ``content_type`` - An alias for ``mimetype``. Historically, this parameter was only called - ``mimetype``, but since this is actually the value included in the HTTP - ``Content-Type`` header, it can also include the character set encoding, - which makes it more than just a MIME type specification. If ``mimetype`` - is specified (not ``None``), that value is used. Otherwise, - ``content_type`` is used. If neither is given, + + .. versionchanged:: 1.5 + + Historically, this parameter was only called ``mimetype`` (now + deprecated), but since this is actually the value included in the HTTP + ``Content-Type`` header, it can also include the character set + encoding, which makes it more than just a MIME type specification. If + ``mimetype`` is specified (not ``None``), that value is used. + Otherwise, ``content_type`` is used. If neither is given, :setting:`DEFAULT_CONTENT_TYPE` is used. ``current_app`` diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt index b1b4700b73..68860f123f 100644 --- a/docs/topics/http/shortcuts.txt +++ b/docs/topics/http/shortcuts.txt @@ -50,6 +50,9 @@ Optional arguments The MIME type to use for the resulting document. Defaults to the value of the :setting:`DEFAULT_CONTENT_TYPE` setting. + .. versionchanged:: 1.5 + This parameter used to be called ``mimetype``. + ``status`` The status code for the response. Defaults to ``200``. @@ -87,7 +90,7 @@ This example is equivalent to:: ``render_to_response`` ====================== -.. function:: render_to_response(template_name[, dictionary][, context_instance][, mimetype]) +.. function:: render_to_response(template_name[, dictionary][, context_instance][, content_type]) Renders a given template with a given context dictionary and returns an :class:`~django.http.HttpResponse` object with that rendered text. @@ -121,10 +124,14 @@ Optional arguments my_data_dictionary, context_instance=RequestContext(request)) -``mimetype`` +``content_type`` The MIME type to use for the resulting document. Defaults to the value of the :setting:`DEFAULT_CONTENT_TYPE` setting. + .. versionchanged:: 1.5 + This parameter used to be called ``mimetype``. + + Example ------- @@ -148,7 +155,7 @@ This example is equivalent to:: t = loader.get_template('myapp/template.html') c = Context({'foo': 'bar'}) return HttpResponse(t.render(c), - mimetype="application/xhtml+xml") + content_type="application/xhtml+xml") ``redirect`` ============ diff --git a/tests/regressiontests/views/generic_urls.py b/tests/regressiontests/views/generic_urls.py index 0f214d12d5..d50436279a 100644 --- a/tests/regressiontests/views/generic_urls.py +++ b/tests/regressiontests/views/generic_urls.py @@ -47,7 +47,7 @@ urlpatterns += patterns('', urlpatterns += patterns('regressiontests.views.views', (r'^shortcuts/render_to_response/$', 'render_to_response_view'), (r'^shortcuts/render_to_response/request_context/$', 'render_to_response_view_with_request_context'), - (r'^shortcuts/render_to_response/mimetype/$', 'render_to_response_view_with_mimetype'), + (r'^shortcuts/render_to_response/content_type/$', 'render_to_response_view_with_content_type'), (r'^shortcuts/render/$', 'render_view'), (r'^shortcuts/render/base_context/$', 'render_view_with_base_context'), (r'^shortcuts/render/content_type/$', 'render_view_with_content_type'), diff --git a/tests/regressiontests/views/tests/shortcuts.py b/tests/regressiontests/views/tests/shortcuts.py index 62bd82f4a7..3a5df6a9cb 100644 --- a/tests/regressiontests/views/tests/shortcuts.py +++ b/tests/regressiontests/views/tests/shortcuts.py @@ -21,8 +21,8 @@ class ShortcutTests(TestCase): self.assertEqual(response.content, b'FOO.BAR../path/to/static/media/\n') self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') - def test_render_to_response_with_mimetype(self): - response = self.client.get('/shortcuts/render_to_response/mimetype/') + def test_render_to_response_with_content_type(self): + response = self.client.get('/shortcuts/render_to_response/content_type/') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'FOO.BAR..\n') self.assertEqual(response['Content-Type'], 'application/x-rendertest') diff --git a/tests/regressiontests/views/views.py b/tests/regressiontests/views/views.py index 748f07637f..50ad98ac2d 100644 --- a/tests/regressiontests/views/views.py +++ b/tests/regressiontests/views/views.py @@ -68,11 +68,11 @@ def render_to_response_view_with_request_context(request): 'bar': 'BAR', }, context_instance=RequestContext(request)) -def render_to_response_view_with_mimetype(request): +def render_to_response_view_with_content_type(request): return render_to_response('debug/render_test.html', { 'foo': 'FOO', 'bar': 'BAR', - }, mimetype='application/x-rendertest') + }, content_type='application/x-rendertest') def render_view(request): return render(request, 'debug/render_test.html', { @@ -263,4 +263,4 @@ class Klass(object): return technical_500_response(request, *exc_info) def sensitive_method_view(request): - return Klass().method(request) \ No newline at end of file + return Klass().method(request) -- cgit v1.3 From 7947c9e3a6bd8c1dfe7fc209fb2256a528149bb6 Mon Sep 17 00:00:00 2001 From: Ramiro Morales Date: Thu, 31 Jan 2013 14:56:26 -0300 Subject: Deprecated undocumented warnings manipulation testing tools. --- django/test/testcases.py | 21 ++++++++++++++------- django/test/utils.py | 7 +++++++ docs/internals/deprecation.txt | 8 +++++++- docs/topics/testing/overview.txt | 2 ++ tests/regressiontests/test_utils/tests.py | 31 +++++++++++++++++-------------- 5 files changed, 47 insertions(+), 22 deletions(-) (limited to 'docs/internals') diff --git a/django/test/testcases.py b/django/test/testcases.py index c311540fc3..3aa0afa35e 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -1,12 +1,13 @@ from __future__ import unicode_literals +from copy import copy import difflib +import errno +from functools import wraps import json import os import re import sys -from copy import copy -from functools import wraps try: from urllib.parse import urlsplit, urlunsplit except ImportError: # Python 2 @@ -14,7 +15,7 @@ except ImportError: # Python 2 import select import socket import threading -import errno +import warnings from django.conf import settings from django.contrib.staticfiles.handlers import StaticFilesHandler @@ -36,8 +37,7 @@ from django.test import _doctest as doctest from django.test.client import Client from django.test.html import HTMLParseError, parse_html from django.test.signals import template_rendered -from django.test.utils import (get_warnings_state, restore_warnings_state, - override_settings, compare_xml, strip_quotes) +from django.test.utils import (override_settings, compare_xml, strip_quotes) from django.test.utils import ContextList from django.utils import unittest as ut2 from django.utils.encoding import force_text @@ -241,6 +241,11 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): class SimpleTestCase(ut2.TestCase): + + _warn_txt = ("save_warnings_state/restore_warnings_state " + "django.test.*TestCase methods are deprecated. Use Python's " + "warnings.catch_warnings context manager instead.") + def __call__(self, result=None): """ Wrapper around default __call__ method to perform common Django test @@ -279,14 +284,16 @@ class SimpleTestCase(ut2.TestCase): """ Saves the state of the warnings module """ - self._warnings_state = get_warnings_state() + warnings.warn(self._warn_txt, DeprecationWarning, stacklevel=2) + self._warnings_state = warnings.filters[:] def restore_warnings_state(self): """ Restores the state of the warnings module to the state saved by save_warnings_state() """ - restore_warnings_state(self._warnings_state) + warnings.warn(self._warn_txt, DeprecationWarning, stacklevel=2) + warnings.filters = self._warnings_state[:] def settings(self, **kwargs): """ diff --git a/django/test/utils.py b/django/test/utils.py index 8114ae0e6a..9413ea8dc4 100644 --- a/django/test/utils.py +++ b/django/test/utils.py @@ -98,6 +98,11 @@ def teardown_test_environment(): del mail.outbox +warn_txt = ("get_warnings_state/restore_warnings_state functions from " + "django.test.utils are deprecated. Use Python's warnings.catch_warnings() " + "context manager instead.") + + def get_warnings_state(): """ Returns an object containing the state of the warnings module @@ -105,6 +110,7 @@ def get_warnings_state(): # There is no public interface for doing this, but this implementation of # get_warnings_state and restore_warnings_state appears to work on Python # 2.4 to 2.7. + warnings.warn(warn_txt, DeprecationWarning, stacklevel=2) return warnings.filters[:] @@ -113,6 +119,7 @@ def restore_warnings_state(state): Restores the state of the warnings module when passed an object that was returned by get_warnings_state() """ + warnings.warn(warn_txt, DeprecationWarning, stacklevel=2) warnings.filters = state[:] diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index da0d1e212c..df3d84fdae 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -267,7 +267,6 @@ these changes. in 1.4. The backward compatibility will be removed -- ``HttpRequest.raw_post_data`` will no longer work. - * The value for the ``post_url_continue`` parameter in ``ModelAdmin.response_add()`` will have to be either ``None`` (to redirect to the newly created object's edit page) or a pre-formatted url. String @@ -314,6 +313,13 @@ these changes. * The ``depth`` keyword argument will be removed from :meth:`~django.db.models.query.QuerySet.select_related`. +* The undocumented ``get_warnings_state()``/``restore_warnings_state()`` + functions from :mod:`django.test.utils` and the ``save_warnings_state()``/ + ``restore_warnings_state()`` + :ref:`django.test.*TestCase ` methods are + deprecated. Use the :class:`warnings.catch_warnings` context manager + available starting with Python 2.6 instead. + 1.8 --- diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index 534569efeb..5739061dd1 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -835,6 +835,8 @@ The following is a simple unit test using the test client:: :class:`django.test.client.RequestFactory` +.. _django-testcase-subclasses: + Provided test case classes -------------------------- diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py index d5d49b2104..2a74b95ffa 100644 --- a/tests/regressiontests/test_utils/tests.py +++ b/tests/regressiontests/test_utils/tests.py @@ -220,24 +220,27 @@ class SaveRestoreWarningState(TestCase): # of save_warnings_state/restore_warnings_state (e.g. just # warnings.resetwarnings()) , but it is difficult to test more. import warnings - self.save_warnings_state() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) - class MyWarning(Warning): - pass + self.save_warnings_state() + + class MyWarning(Warning): + pass - # Add a filter that causes an exception to be thrown, so we can catch it - warnings.simplefilter("error", MyWarning) - self.assertRaises(Warning, lambda: warnings.warn("warn", MyWarning)) + # Add a filter that causes an exception to be thrown, so we can catch it + warnings.simplefilter("error", MyWarning) + self.assertRaises(Warning, lambda: warnings.warn("warn", MyWarning)) - # Now restore. - self.restore_warnings_state() - # After restoring, we shouldn't get an exception. But we don't want a - # warning printed either, so we have to silence the warning. - warnings.simplefilter("ignore", MyWarning) - warnings.warn("warn", MyWarning) + # Now restore. + self.restore_warnings_state() + # After restoring, we shouldn't get an exception. But we don't want a + # warning printed either, so we have to silence the warning. + warnings.simplefilter("ignore", MyWarning) + warnings.warn("warn", MyWarning) - # Remove the filter we just added. - self.restore_warnings_state() + # Remove the filter we just added. + self.restore_warnings_state() class HTMLEqualTests(TestCase): -- cgit v1.3 From ab51dff83d100af6ef026cab8cb75134e28296eb Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Fri, 1 Feb 2013 14:52:27 -0500 Subject: Added myself to core developpers --- AUTHORS | 2 +- docs/internals/committers.txt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) (limited to 'docs/internals') diff --git a/AUTHORS b/AUTHORS index 8eb500d3e1..6e79befd31 100644 --- a/AUTHORS +++ b/AUTHORS @@ -34,6 +34,7 @@ The PRIMARY AUTHORS are (and/or have been): * Jeremy Dunck * Bryan Veloso * Preston Holmes + * Simon Charette More information on the main contributors to Django can be found in docs/internals/committers.txt. @@ -127,7 +128,6 @@ answer newbie questions, and generally made Django that much better: Chris Chamberlin Amit Chakradeo ChaosKCW - Simon Charette Kowito Charoenratchatabhan Sengtha Chay ivan.chelubeev@gmail.com diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 7900dd8cd0..69c9974967 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -419,6 +419,22 @@ Jeremy Dunck .. _Preston Holmes: http://www.ptone.com/ +`Simon Charette`_ + Simon is a mathematic student who discovered Django while searching for a + replacement framework to an in-house PHP entity. Since that faithful day + Django has been a big part of his life. So far, he's been involved in some + ORM and forms API fixes. + + Apart from contributing to multiple open source projects he spends most of + his spare-time playing `Ultimate Frisbee`_ and working part-time + at this awesome place called `Reptiletech`_. + + Simon lives in Montréal, Québec, Canada. + +.. _Simon Charette: https://github.com/charettes +.. _Ultimate Frisbee: http://www.montrealultimate.ca +.. _Reptiletech: http://www.reptiletech.com + Specialists ----------- -- cgit v1.3 From c9c40bc6bc64e67365338751e4967d86d0882abf Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Sat, 2 Feb 2013 13:53:43 -0800 Subject: Fixed #19333 -- Moved compress.py outside of the admin static folder. Thanks to camilonova, Russell Keith-Magee, Aymeric Augustin and Ramiro Morales for the feedback. --- django/contrib/admin/bin/compress.py | 47 ++++++++++++++++++++++ django/contrib/admin/static/admin/js/compress.py | 47 ---------------------- .../writing-code/submitting-patches.txt | 6 ++- 3 files changed, 51 insertions(+), 49 deletions(-) create mode 100644 django/contrib/admin/bin/compress.py delete mode 100644 django/contrib/admin/static/admin/js/compress.py (limited to 'docs/internals') diff --git a/django/contrib/admin/bin/compress.py b/django/contrib/admin/bin/compress.py new file mode 100644 index 0000000000..e15f2d3ef6 --- /dev/null +++ b/django/contrib/admin/bin/compress.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +import os +import optparse +import subprocess +import sys + +js_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static', 'admin', 'js') + +def main(): + usage = "usage: %prog [file1..fileN]" + description = """With no file paths given this script will automatically +compress all jQuery-based files of the admin app. Requires the Google Closure +Compiler library and Java version 6 or later.""" + parser = optparse.OptionParser(usage, description=description) + parser.add_option("-c", dest="compiler", default="~/bin/compiler.jar", + help="path to Closure Compiler jar file") + parser.add_option("-v", "--verbose", + action="store_true", dest="verbose") + parser.add_option("-q", "--quiet", + action="store_false", dest="verbose") + (options, args) = parser.parse_args() + + compiler = os.path.expanduser(options.compiler) + if not os.path.exists(compiler): + sys.exit("Google Closure compiler jar file %s not found. Please use the -c option to specify the path." % compiler) + + if not args: + if options.verbose: + sys.stdout.write("No filenames given; defaulting to admin scripts\n") + args = [os.path.join(js_path, f) for f in [ + "actions.js", "collapse.js", "inlines.js", "prepopulate.js"]] + + for arg in args: + if not arg.endswith(".js"): + arg = arg + ".js" + to_compress = os.path.expanduser(arg) + if os.path.exists(to_compress): + to_compress_min = "%s.min.js" % "".join(arg.rsplit(".js")) + cmd = "java -jar %s --js %s --js_output_file %s" % (compiler, to_compress, to_compress_min) + if options.verbose: + sys.stdout.write("Running: %s\n" % cmd) + subprocess.call(cmd.split()) + else: + sys.stdout.write("File %s not found. Sure it exists?\n" % to_compress) + +if __name__ == '__main__': + main() diff --git a/django/contrib/admin/static/admin/js/compress.py b/django/contrib/admin/static/admin/js/compress.py deleted file mode 100644 index 8d2caa28ea..0000000000 --- a/django/contrib/admin/static/admin/js/compress.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python -import os -import optparse -import subprocess -import sys - -here = os.path.dirname(__file__) - -def main(): - usage = "usage: %prog [file1..fileN]" - description = """With no file paths given this script will automatically -compress all jQuery-based files of the admin app. Requires the Google Closure -Compiler library and Java version 6 or later.""" - parser = optparse.OptionParser(usage, description=description) - parser.add_option("-c", dest="compiler", default="~/bin/compiler.jar", - help="path to Closure Compiler jar file") - parser.add_option("-v", "--verbose", - action="store_true", dest="verbose") - parser.add_option("-q", "--quiet", - action="store_false", dest="verbose") - (options, args) = parser.parse_args() - - compiler = os.path.expanduser(options.compiler) - if not os.path.exists(compiler): - sys.exit("Google Closure compiler jar file %s not found. Please use the -c option to specify the path." % compiler) - - if not args: - if options.verbose: - sys.stdout.write("No filenames given; defaulting to admin scripts\n") - args = [os.path.join(here, f) for f in [ - "actions.js", "collapse.js", "inlines.js", "prepopulate.js"]] - - for arg in args: - if not arg.endswith(".js"): - arg = arg + ".js" - to_compress = os.path.expanduser(arg) - if os.path.exists(to_compress): - to_compress_min = "%s.min.js" % "".join(arg.rsplit(".js")) - cmd = "java -jar %s --js %s --js_output_file %s" % (compiler, to_compress, to_compress_min) - if options.verbose: - sys.stdout.write("Running: %s\n" % cmd) - subprocess.call(cmd.split()) - else: - sys.stdout.write("File %s not found. Sure it exists?\n" % to_compress) - -if __name__ == '__main__': - main() diff --git a/docs/internals/contributing/writing-code/submitting-patches.txt b/docs/internals/contributing/writing-code/submitting-patches.txt index a90dc32605..ed8aad99b3 100644 --- a/docs/internals/contributing/writing-code/submitting-patches.txt +++ b/docs/internals/contributing/writing-code/submitting-patches.txt @@ -176,8 +176,10 @@ Compressing JavaScript ~~~~~~~~~~~~~~~~~~~~~~ To simplify the process of providing optimized javascript code, Django -includes a handy script which should be used to create a "minified" version. -This script is located at ``django/contrib/admin/static/admin/js/compress.py``. +includes a handy python script which should be used to create a "minified" +version. To run it:: + + python django/contrib/admin/bin/compress.py Behind the scenes, ``compress.py`` is a front-end for Google's `Closure Compiler`_ which is written in Java. However, the Closure Compiler -- cgit v1.3 From ec469ade2b04b94bfeb59fb0fc7d9300470be615 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Tue, 5 Feb 2013 04:16:07 -0500 Subject: Fixed #19689 -- Renamed `Model._meta.module_name` to `model_name`. --- django/contrib/admin/actions.py | 2 +- django/contrib/admin/options.py | 24 +++++++++--------- django/contrib/admin/sites.py | 6 ++--- .../templates/admin/auth/user/change_password.html | 2 +- .../contrib/admin/templates/admin/change_form.html | 2 +- django/contrib/admin/templatetags/admin_urls.py | 3 +-- django/contrib/admin/util.py | 2 +- django/contrib/admin/views/main.py | 2 +- django/contrib/admin/widgets.py | 4 +-- django/contrib/admindocs/views.py | 12 ++++----- django/contrib/auth/context_processors.py | 16 ++++++------ django/contrib/auth/management/__init__.py | 2 +- django/contrib/comments/moderation.py | 4 +-- django/contrib/comments/views/comments.py | 4 +-- django/contrib/contenttypes/generic.py | 4 +-- django/contrib/contenttypes/management.py | 2 +- django/contrib/contenttypes/models.py | 10 ++++---- django/contrib/gis/sitemaps/kml.py | 2 +- django/core/cache/backends/db.py | 2 +- django/core/management/sql.py | 4 +-- django/core/serializers/python.py | 2 +- django/core/xheaders.py | 2 +- django/db/models/base.py | 4 +-- django/db/models/fields/related.py | 8 +++--- django/db/models/loading.py | 2 +- django/db/models/options.py | 29 +++++++++++++++------- django/db/models/related.py | 10 ++++---- django/views/generic/detail.py | 6 ++--- django/views/generic/list.py | 4 +-- docs/internals/deprecation.txt | 2 ++ docs/releases/1.6.txt | 6 +++++ tests/regressiontests/admin_custom_urls/models.py | 2 +- 32 files changed, 102 insertions(+), 84 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py index 201101736e..d11ba3d1a8 100644 --- a/django/contrib/admin/actions.py +++ b/django/contrib/admin/actions.py @@ -75,7 +75,7 @@ def delete_selected(modeladmin, request, queryset): # Display the confirmation page return TemplateResponse(request, modeladmin.delete_selected_confirmation_template or [ - "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.object_name.lower()), + "admin/%s/%s/delete_selected_confirmation.html" % (app_label, opts.model_name), "admin/%s/delete_selected_confirmation.html" % app_label, "admin/delete_selected_confirmation.html" ], context, current_app=modeladmin.admin_site.name) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 8e0aaccc86..8de31121e0 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -371,7 +371,7 @@ class ModelAdmin(BaseModelAdmin): return self.admin_site.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) - info = self.model._meta.app_label, self.model._meta.module_name + info = self.model._meta.app_label, self.model._meta.model_name urlpatterns = patterns('', url(r'^$', @@ -783,7 +783,7 @@ class ModelAdmin(BaseModelAdmin): form_template = self.change_form_template return TemplateResponse(request, form_template or [ - "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), + "admin/%s/%s/change_form.html" % (app_label, opts.model_name), "admin/%s/change_form.html" % app_label, "admin/change_form.html" ], context, current_app=self.admin_site.name) @@ -803,7 +803,7 @@ class ModelAdmin(BaseModelAdmin): self.message_user(request, msg) if post_url_continue is None: post_url_continue = reverse('admin:%s_%s_change' % - (opts.app_label, opts.module_name), + (opts.app_label, opts.model_name), args=(pk_value,), current_app=self.admin_site.name) if "_popup" in request.POST: @@ -845,14 +845,14 @@ class ModelAdmin(BaseModelAdmin): msg = _('The %(name)s "%(obj)s" was added successfully. You may edit it again below.') % msg_dict self.message_user(request, msg) return HttpResponseRedirect(reverse('admin:%s_%s_change' % - (opts.app_label, opts.module_name), + (opts.app_label, opts.model_name), args=(pk_value,), current_app=self.admin_site.name)) elif "_addanother" in request.POST: msg = _('The %(name)s "%(obj)s" was changed successfully. You may add another %(name)s below.') % msg_dict self.message_user(request, msg) return HttpResponseRedirect(reverse('admin:%s_%s_add' % - (opts.app_label, opts.module_name), + (opts.app_label, opts.model_name), current_app=self.admin_site.name)) else: msg = _('The %(name)s "%(obj)s" was changed successfully.') % msg_dict @@ -867,7 +867,7 @@ class ModelAdmin(BaseModelAdmin): opts = self.model._meta if self.has_change_permission(request, None): post_url = reverse('admin:%s_%s_changelist' % - (opts.app_label, opts.module_name), + (opts.app_label, opts.model_name), current_app=self.admin_site.name) else: post_url = reverse('admin:index', @@ -882,7 +882,7 @@ class ModelAdmin(BaseModelAdmin): opts = self.model._meta if self.has_change_permission(request, None): post_url = reverse('admin:%s_%s_changelist' % - (opts.app_label, opts.module_name), + (opts.app_label, opts.model_name), current_app=self.admin_site.name) else: post_url = reverse('admin:index', @@ -1060,7 +1060,7 @@ class ModelAdmin(BaseModelAdmin): if request.method == 'POST' and "_saveasnew" in request.POST: return self.add_view(request, form_url=reverse('admin:%s_%s_add' % - (opts.app_label, opts.module_name), + (opts.app_label, opts.model_name), current_app=self.admin_site.name)) ModelForm = self.get_form(request, obj) @@ -1283,7 +1283,7 @@ class ModelAdmin(BaseModelAdmin): context.update(extra_context or {}) return TemplateResponse(request, self.change_list_template or [ - 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()), + 'admin/%s/%s/change_list.html' % (app_label, opts.model_name), 'admin/%s/change_list.html' % app_label, 'admin/change_list.html' ], context, current_app=self.admin_site.name) @@ -1323,7 +1323,7 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(reverse('admin:index', current_app=self.admin_site.name)) return HttpResponseRedirect(reverse('admin:%s_%s_changelist' % - (opts.app_label, opts.module_name), + (opts.app_label, opts.model_name), current_app=self.admin_site.name)) object_name = force_text(opts.verbose_name) @@ -1346,7 +1346,7 @@ class ModelAdmin(BaseModelAdmin): context.update(extra_context or {}) return TemplateResponse(request, self.delete_confirmation_template or [ - "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), + "admin/%s/%s/delete_confirmation.html" % (app_label, opts.model_name), "admin/%s/delete_confirmation.html" % app_label, "admin/delete_confirmation.html" ], context, current_app=self.admin_site.name) @@ -1373,7 +1373,7 @@ class ModelAdmin(BaseModelAdmin): } context.update(extra_context or {}) return TemplateResponse(request, self.object_history_template or [ - "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()), + "admin/%s/%s/object_history.html" % (app_label, opts.model_name), "admin/%s/object_history.html" % app_label, "admin/object_history.html" ], context, current_app=self.admin_site.name) diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 185417015a..07d1ec7804 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -247,7 +247,7 @@ class AdminSite(object): # Add in each model's views. for model, model_admin in six.iteritems(self._registry): urlpatterns += patterns('', - url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name), + url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)) ) return urlpatterns @@ -351,7 +351,7 @@ class AdminSite(object): # Check whether user has any perm for this module. # If so, add the module to the model_list. if True in perms.values(): - info = (app_label, model._meta.module_name) + info = (app_label, model._meta.model_name) model_dict = { 'name': capfirst(model._meta.verbose_name_plural), 'object_name': model._meta.object_name, @@ -407,7 +407,7 @@ class AdminSite(object): # Check whether user has any perm for this module. # If so, add the module to the model_list. if True in perms.values(): - info = (app_label, model._meta.module_name) + info = (app_label, model._meta.model_name) model_dict = { 'name': capfirst(model._meta.verbose_name_plural), 'object_name': model._meta.object_name, diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index 83a9c48ee1..9d1b917b61 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -19,7 +19,7 @@ {% endblock %} {% endif %} {% block content %}
    -
    {% csrf_token %}{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
    {% if is_popup %}{% endif %} {% if form.errors %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index 48846960b3..daf37753dc 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -35,7 +35,7 @@
{% endif %}{% endif %} {% endblock %} -{% csrf_token %}{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} {% if save_on_top %}{% block submit_buttons_top %}{% submit_row %}{% endblock %}{% endif %} diff --git a/django/contrib/admin/templatetags/admin_urls.py b/django/contrib/admin/templatetags/admin_urls.py index 90e81b0ef3..bca95d92ae 100644 --- a/django/contrib/admin/templatetags/admin_urls.py +++ b/django/contrib/admin/templatetags/admin_urls.py @@ -1,4 +1,3 @@ -from django.core.urlresolvers import reverse from django import template from django.contrib.admin.util import quote @@ -6,7 +5,7 @@ register = template.Library() @register.filter def admin_urlname(value, arg): - return 'admin:%s_%s_%s' % (value.app_label, value.module_name, arg) + return 'admin:%s_%s_%s' % (value.app_label, value.model_name, arg) @register.filter diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 07013d1d4b..133a8ad13e 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -116,7 +116,7 @@ def get_deleted_objects(objs, opts, user, admin_site, using): admin_url = reverse('%s:%s_%s_change' % (admin_site.name, opts.app_label, - opts.object_name.lower()), + opts.model_name), None, (quote(obj._get_pk_val()),)) p = '%s.%s' % (opts.app_label, opts.get_delete_permission()) diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index be7067ff61..4b296b3f4f 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -379,6 +379,6 @@ class ChangeList(object): def url_for_result(self, result): pk = getattr(result, self.pk_attname) return reverse('admin:%s_%s_change' % (self.opts.app_label, - self.opts.module_name), + self.opts.model_name), args=(quote(pk),), current_app=self.model_admin.admin_site.name) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index a3887740d8..4b79401dbc 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -147,7 +147,7 @@ class ForeignKeyRawIdWidget(forms.TextInput): # The related object is registered with the same AdminSite related_url = reverse('admin:%s_%s_changelist' % (rel_to._meta.app_label, - rel_to._meta.module_name), + rel_to._meta.model_name), current_app=self.admin_site.name) params = self.url_parameters() @@ -247,7 +247,7 @@ class RelatedFieldWidgetWrapper(forms.Widget): def render(self, name, value, *args, **kwargs): rel_to = self.rel.to - info = (rel_to._meta.app_label, rel_to._meta.object_name.lower()) + info = (rel_to._meta.app_label, rel_to._meta.model_name) self.widget.choices = self.choices output = [self.widget.render(name, value, *args, **kwargs)] if self.can_add_related: diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index cb0c116416..ef2790f2db 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -189,7 +189,7 @@ def model_detail(request, app_label, model_name): raise Http404(_("App %r not found") % app_label) model = None for m in models.get_models(app_mod): - if m._meta.object_name.lower() == model_name: + if m._meta.model_name == model_name: model = m break if model is None: @@ -224,12 +224,12 @@ def model_detail(request, app_label, model_name): fields.append({ 'name': "%s.all" % field.name, "data_type": 'List', - 'verbose': utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose': utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.model_name), }) fields.append({ 'name' : "%s.count" % field.name, 'data_type' : 'Integer', - 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.model_name), }) # Gather model methods. @@ -243,7 +243,7 @@ def model_detail(request, app_label, model_name): continue verbose = func.__doc__ if verbose: - verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.module_name) + verbose = utils.parse_rst(utils.trim_docstring(verbose), 'model', _('model:') + opts.model_name) fields.append({ 'name': func_name, 'data_type': get_return_data_type(func_name), @@ -257,12 +257,12 @@ def model_detail(request, app_label, model_name): fields.append({ 'name' : "%s.all" % accessor, 'data_type' : 'List', - 'verbose' : utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose' : utils.parse_rst(_("all %s") % verbose , 'model', _('model:') + opts.model_name), }) fields.append({ 'name' : "%s.count" % accessor, 'data_type' : 'Integer', - 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.module_name), + 'verbose' : utils.parse_rst(_("number of %s") % verbose , 'model', _('model:') + opts.model_name), }) return render_to_response('admin_doc/model_detail.html', { 'root_path': urlresolvers.reverse('admin:index'), diff --git a/django/contrib/auth/context_processors.py b/django/contrib/auth/context_processors.py index 3d17fe2754..b8ead73eb4 100644 --- a/django/contrib/auth/context_processors.py +++ b/django/contrib/auth/context_processors.py @@ -2,14 +2,14 @@ # the template system can understand. class PermLookupDict(object): - def __init__(self, user, module_name): - self.user, self.module_name = user, module_name + def __init__(self, user, app_label): + self.user, self.app_label = user, app_label def __repr__(self): return str(self.user.get_all_permissions()) def __getitem__(self, perm_name): - return self.user.has_perm("%s.%s" % (self.module_name, perm_name)) + return self.user.has_perm("%s.%s" % (self.app_label, perm_name)) def __iter__(self): # To fix 'item in perms.someapp' and __getitem__ iteraction we need to @@ -17,7 +17,7 @@ class PermLookupDict(object): raise TypeError("PermLookupDict is not iterable.") def __bool__(self): - return self.user.has_module_perms(self.module_name) + return self.user.has_module_perms(self.app_label) def __nonzero__(self): # Python 2 compatibility return type(self).__bool__(self) @@ -27,8 +27,8 @@ class PermWrapper(object): def __init__(self, user): self.user = user - def __getitem__(self, module_name): - return PermLookupDict(self.user, module_name) + def __getitem__(self, app_label): + return PermLookupDict(self.user, app_label) def __iter__(self): # I am large, I contain multitudes. @@ -41,8 +41,8 @@ class PermWrapper(object): if '.' not in perm_name: # The name refers to module. return bool(self[perm_name]) - module_name, perm_name = perm_name.split('.', 1) - return self[module_name][perm_name] + app_label, perm_name = perm_name.split('.', 1) + return self[app_label][perm_name] def auth(request): diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index a77bba0f73..475dd255d4 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -17,7 +17,7 @@ from django.utils.six.moves import input def _get_permission_codename(action, opts): - return '%s_%s' % (action, opts.object_name.lower()) + return '%s_%s' % (action, opts.model_name) def _get_all_permissions(opts, ctype): diff --git a/django/contrib/comments/moderation.py b/django/contrib/comments/moderation.py index 6c56d7a8a5..6648aebb59 100644 --- a/django/contrib/comments/moderation.py +++ b/django/contrib/comments/moderation.py @@ -302,7 +302,7 @@ class Moderator(object): model_or_iterable = [model_or_iterable] for model in model_or_iterable: if model in self._registry: - raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.module_name) + raise AlreadyModerated("The model '%s' is already being moderated" % model._meta.model_name) self._registry[model] = moderation_class(model) def unregister(self, model_or_iterable): @@ -318,7 +318,7 @@ class Moderator(object): model_or_iterable = [model_or_iterable] for model in model_or_iterable: if model not in self._registry: - raise NotModerated("The model '%s' is not currently being moderated" % model._meta.module_name) + raise NotModerated("The model '%s' is not currently being moderated" % model._meta.model_name) del self._registry[model] def pre_save_moderation(self, sender, comment, request, **kwargs): diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 7c02b21b6a..befd326092 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -86,10 +86,10 @@ def post_comment(request, next=None, using=None): # These first two exist for purely historical reasons. # Django v1.0 and v1.1 allowed the underscore format for # preview templates, so we have to preserve that format. - "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.module_name), + "comments/%s_%s_preview.html" % (model._meta.app_label, model._meta.model_name), "comments/%s_preview.html" % model._meta.app_label, # Now the usual directory based template hierarchy. - "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.module_name), + "comments/%s/%s/preview.html" % (model._meta.app_label, model._meta.model_name), "comments/%s/preview.html" % model._meta.app_label, "comments/preview.html", ] diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index cda4d46fe8..d849d1607e 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -389,7 +389,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet): opts = self.model._meta self.instance = instance self.rel_name = '-'.join(( - opts.app_label, opts.object_name.lower(), + opts.app_label, opts.model_name, self.ct_field.name, self.ct_fk_field.name, )) if self.instance is None or self.instance.pk is None: @@ -409,7 +409,7 @@ class BaseGenericInlineFormSet(BaseModelFormSet): @classmethod def get_default_prefix(cls): opts = cls.model._meta - return '-'.join((opts.app_label, opts.object_name.lower(), + return '-'.join((opts.app_label, opts.model_name, cls.ct_field.name, cls.ct_fk_field.name, )) diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index 8329ab65d9..ddd7654ed7 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -21,7 +21,7 @@ def update_contenttypes(app, created_models, verbosity=2, db=DEFAULT_DB_ALIAS, * # They all have the same app_label, get the first one. app_label = app_models[0]._meta.app_label app_models = dict( - (model._meta.object_name.lower(), model) + (model._meta.model_name, model) for model in app_models ) diff --git a/django/contrib/contenttypes/models.py b/django/contrib/contenttypes/models.py index b658655bbb..f0bd109b00 100644 --- a/django/contrib/contenttypes/models.py +++ b/django/contrib/contenttypes/models.py @@ -25,7 +25,7 @@ class ContentTypeManager(models.Manager): return model._meta def _get_from_cache(self, opts): - key = (opts.app_label, opts.object_name.lower()) + key = (opts.app_label, opts.model_name) return self.__class__._cache[self.db][key] def get_for_model(self, model, for_concrete_model=True): @@ -43,7 +43,7 @@ class ContentTypeManager(models.Manager): # django.utils.functional.__proxy__ object. ct, created = self.get_or_create( app_label = opts.app_label, - model = opts.object_name.lower(), + model = opts.model_name, defaults = {'name': smart_text(opts.verbose_name_raw)}, ) self._add_to_cache(self.db, ct) @@ -67,7 +67,7 @@ class ContentTypeManager(models.Manager): ct = self._get_from_cache(opts) except KeyError: needed_app_labels.add(opts.app_label) - needed_models.add(opts.object_name.lower()) + needed_models.add(opts.model_name) needed_opts.add(opts) else: results[model] = ct @@ -86,7 +86,7 @@ class ContentTypeManager(models.Manager): # These weren't in the cache, or the DB, create them. ct = self.create( app_label=opts.app_label, - model=opts.object_name.lower(), + model=opts.model_name, name=smart_text(opts.verbose_name_raw), ) self._add_to_cache(self.db, ct) @@ -119,7 +119,7 @@ class ContentTypeManager(models.Manager): def _add_to_cache(self, using, ct): """Insert a ContentType into the cache.""" model = ct.model_class() - key = (model._meta.app_label, model._meta.object_name.lower()) + key = (model._meta.app_label, model._meta.model_name) self.__class__._cache.setdefault(using, {})[key] = ct self.__class__._cache.setdefault(using, {})[ct.id] = ct diff --git a/django/contrib/gis/sitemaps/kml.py b/django/contrib/gis/sitemaps/kml.py index db30606b04..837fe62b62 100644 --- a/django/contrib/gis/sitemaps/kml.py +++ b/django/contrib/gis/sitemaps/kml.py @@ -30,7 +30,7 @@ class KMLSitemap(Sitemap): for field in source._meta.fields: if isinstance(field, GeometryField): kml_sources.append((source._meta.app_label, - source._meta.module_name, + source._meta.model_name, field.name)) elif isinstance(source, (list, tuple)): if len(source) != 3: diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index c93bc90b18..5c9ea3e7bb 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -23,7 +23,7 @@ class Options(object): def __init__(self, table): self.db_table = table self.app_label = 'django_cache' - self.module_name = 'cacheentry' + self.model_name = 'cacheentry' self.verbose_name = 'cache entry' self.verbose_name_plural = 'cache entries' self.object_name = 'CacheEntry' diff --git a/django/core/management/sql.py b/django/core/management/sql.py index e46f4ae4f5..66df43e971 100644 --- a/django/core/management/sql.py +++ b/django/core/management/sql.py @@ -173,8 +173,8 @@ def custom_sql_for_model(model, style, connection): # Find custom SQL, if it's available. backend_name = connection.settings_dict['ENGINE'].split('.')[-1] - sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.object_name.lower(), backend_name)), - os.path.join(app_dir, "%s.sql" % opts.object_name.lower())] + sql_files = [os.path.join(app_dir, "%s.%s.sql" % (opts.model_name, backend_name)), + os.path.join(app_dir, "%s.sql" % opts.model_name)] for sql_file in sql_files: if os.path.exists(sql_file): with codecs.open(sql_file, 'U', encoding=settings.FILE_CHARSET) as fp: diff --git a/django/core/serializers/python.py b/django/core/serializers/python.py index 37fa906280..5e07e2a006 100644 --- a/django/core/serializers/python.py +++ b/django/core/serializers/python.py @@ -143,7 +143,7 @@ def Deserializer(object_list, **options): def _get_model(model_identifier): """ - Helper to look up a model from an "app_label.module_name" string. + Helper to look up a model from an "app_label.model_name" string. """ try: Model = models.get_model(*model_identifier.split(".")) diff --git a/django/core/xheaders.py b/django/core/xheaders.py index b650a3a6d4..3766628c98 100644 --- a/django/core/xheaders.py +++ b/django/core/xheaders.py @@ -20,5 +20,5 @@ def populate_xheaders(request, response, model, object_id): 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.object_name.lower()) + response['X-Object-Type'] = "%s.%s" % (model._meta.app_label, model._meta.model_name) response['X-Object-Id'] = str(object_id) diff --git a/django/db/models/base.py b/django/db/models/base.py index 38afc60991..5f058654bf 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -191,7 +191,7 @@ class ModelBase(type): if base in o2o_map: field = o2o_map[base] elif not is_proxy: - attr_name = '%s_ptr' % base._meta.module_name + attr_name = '%s_ptr' % base._meta.model_name field = OneToOneField(base, name=attr_name, auto_created=True, parent_link=True) new_class.add_to_class(attr_name, field) @@ -973,7 +973,7 @@ def method_get_order(ordered_obj, self): ############################################## def get_absolute_url(opts, func, self, *args, **kwargs): - return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.module_name), func)(self, *args, **kwargs) + return settings.ABSOLUTE_URL_OVERRIDES.get('%s.%s' % (opts.app_label, opts.model_name), func)(self, *args, **kwargs) ######## diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index ae792a30e7..bd2e288410 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -118,7 +118,7 @@ class RelatedField(object): self.do_related_class(other, cls) def set_attributes_from_rel(self): - self.name = self.name or (self.rel.to._meta.object_name.lower() + '_' + self.rel.to._meta.pk.name) + self.name = self.name or (self.rel.to._meta.model_name + '_' + self.rel.to._meta.pk.name) if self.verbose_name is None: self.verbose_name = self.rel.to._meta.verbose_name self.rel.field_name = self.rel.field_name or self.rel.to._meta.pk.name @@ -222,7 +222,7 @@ class RelatedField(object): # related object in a table-spanning query. It uses the lower-cased # object_name by default, but this can be overridden with the # "related_name" option. - return self.rel.related_name or self.opts.object_name.lower() + return self.rel.related_name or self.opts.model_name class SingleRelatedObjectDescriptor(object): @@ -983,7 +983,7 @@ class ForeignKey(RelatedField, Field): def __init__(self, to, to_field=None, rel_class=ManyToOneRel, **kwargs): try: - to_name = to._meta.object_name.lower() + to_name = to._meta.model_name except AttributeError: # to._meta doesn't exist, so it must be RECURSIVE_RELATIONSHIP_CONSTANT assert isinstance(to, six.string_types), "%s(%r) is invalid. First parameter to ForeignKey must be either a model, a model name, or the string %r" % (self.__class__.__name__, to, RECURSIVE_RELATIONSHIP_CONSTANT) else: @@ -1174,7 +1174,7 @@ def create_many_to_many_intermediary_model(field, klass): from_ = 'from_%s' % to.lower() to = 'to_%s' % to.lower() else: - from_ = klass._meta.object_name.lower() + from_ = klass._meta.model_name to = to.lower() meta = type('Meta', (object,), { 'db_table': field._get_m2m_db_table(klass._meta), diff --git a/django/db/models/loading.py b/django/db/models/loading.py index 56edc36bec..c027105c5b 100644 --- a/django/db/models/loading.py +++ b/django/db/models/loading.py @@ -239,7 +239,7 @@ class AppCache(object): for model in models: # Store as 'name: model' pair in a dictionary # in the app_models dictionary - model_name = model._meta.object_name.lower() + model_name = model._meta.model_name model_dict = self.app_models.setdefault(app_label, SortedDict()) if model_name in model_dict: # The same model may be imported via different paths (e.g. diff --git a/django/db/models/options.py b/django/db/models/options.py index 952596b514..a302e2d73a 100644 --- a/django/db/models/options.py +++ b/django/db/models/options.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import re from bisect import bisect +import warnings from django.conf import settings from django.db.models.fields.related import ManyToManyRel @@ -28,7 +29,7 @@ class Options(object): def __init__(self, meta, app_label=None): self.local_fields, self.local_many_to_many = [], [] self.virtual_fields = [] - self.module_name, self.verbose_name = None, None + self.model_name, self.verbose_name = None, None self.verbose_name_plural = None self.db_table = '' self.ordering = [] @@ -78,7 +79,7 @@ class Options(object): self.installed = re.sub('\.models$', '', cls.__module__) in settings.INSTALLED_APPS # First, construct the default values for these options. self.object_name = cls.__name__ - self.module_name = self.object_name.lower() + self.model_name = self.object_name.lower() self.verbose_name = get_verbose_name(self.object_name) # Next, apply any overridden values from 'class Meta'. @@ -116,11 +117,21 @@ class Options(object): self.verbose_name_plural = string_concat(self.verbose_name, 's') del self.meta - # If the db_table wasn't provided, use the app_label + module_name. + # If the db_table wasn't provided, use the app_label + model_name. if not self.db_table: - self.db_table = "%s_%s" % (self.app_label, self.module_name) + self.db_table = "%s_%s" % (self.app_label, self.model_name) self.db_table = truncate_name(self.db_table, connection.ops.max_name_length()) + @property + def module_name(self): + """ + This property has been deprecated in favor of `model_name`. refs #19689 + """ + warnings.warn( + "Options.module_name has been deprecated in favor of model_name", + PendingDeprecationWarning, stacklevel=2) + return self.model_name + def _prepare(self, model): if self.order_with_respect_to: self.order_with_respect_to = self.get_field(self.order_with_respect_to) @@ -193,7 +204,7 @@ class Options(object): return '' % self.object_name def __str__(self): - return "%s.%s" % (smart_text(self.app_label), smart_text(self.module_name)) + return "%s.%s" % (smart_text(self.app_label), smart_text(self.model_name)) def verbose_name_raw(self): """ @@ -217,7 +228,7 @@ class Options(object): case insensitive, so we make sure we are case insensitive here. """ if self.swappable: - model_label = '%s.%s' % (self.app_label, self.object_name.lower()) + model_label = '%s.%s' % (self.app_label, self.model_name) swapped_for = getattr(settings, self.swappable, None) if swapped_for: try: @@ -371,13 +382,13 @@ class Options(object): return cache def get_add_permission(self): - return 'add_%s' % self.object_name.lower() + return 'add_%s' % self.model_name def get_change_permission(self): - return 'change_%s' % self.object_name.lower() + return 'change_%s' % self.model_name def get_delete_permission(self): - return 'delete_%s' % self.object_name.lower() + return 'delete_%s' % self.model_name def get_all_related_objects(self, local_only=False, include_hidden=False, include_proxy_eq=False): diff --git a/django/db/models/related.py b/django/db/models/related.py index 26932137ad..53645bedb9 100644 --- a/django/db/models/related.py +++ b/django/db/models/related.py @@ -16,8 +16,8 @@ class RelatedObject(object): self.model = model self.opts = model._meta self.field = field - self.name = '%s:%s' % (self.opts.app_label, self.opts.module_name) - self.var_name = self.opts.object_name.lower() + self.name = '%s:%s' % (self.opts.app_label, self.opts.model_name) + self.var_name = self.opts.model_name def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH, limit_to_currently_related=False): @@ -31,7 +31,7 @@ class RelatedObject(object): queryset = self.model._default_manager.all() if limit_to_currently_related: queryset = queryset.complex_filter( - {'%s__isnull' % self.parent_model._meta.module_name: False}) + {'%s__isnull' % self.parent_model._meta.model_name: False}) lst = [(x._get_pk_val(), smart_text(x)) for x in queryset] return first_choice + lst @@ -56,9 +56,9 @@ class RelatedObject(object): # If this is a symmetrical m2m relation on self, there is no reverse accessor. if getattr(self.field.rel, 'symmetrical', False) and self.model == self.parent_model: return None - return self.field.rel.related_name or (self.opts.object_name.lower() + '_set') + return self.field.rel.related_name or (self.opts.model_name + '_set') else: - return self.field.rel.related_name or (self.opts.object_name.lower()) + return self.field.rel.related_name or (self.opts.model_name) def get_cache_name(self): return "_%s_cache" % self.get_accessor_name() diff --git a/django/views/generic/detail.py b/django/views/generic/detail.py index c27b92b85e..58302bbe23 100644 --- a/django/views/generic/detail.py +++ b/django/views/generic/detail.py @@ -84,7 +84,7 @@ class SingleObjectMixin(ContextMixin): if self.context_object_name: return self.context_object_name elif isinstance(obj, models.Model): - return obj._meta.object_name.lower() + return obj._meta.model_name else: return None @@ -144,13 +144,13 @@ class SingleObjectTemplateResponseMixin(TemplateResponseMixin): if isinstance(self.object, models.Model): names.append("%s/%s%s.html" % ( self.object._meta.app_label, - self.object._meta.object_name.lower(), + self.object._meta.model_name, self.template_name_suffix )) elif hasattr(self, 'model') and self.model is not None and issubclass(self.model, models.Model): names.append("%s/%s%s.html" % ( self.model._meta.app_label, - self.model._meta.object_name.lower(), + self.model._meta.model_name, self.template_name_suffix )) return names diff --git a/django/views/generic/list.py b/django/views/generic/list.py index 1f286168f6..08c4bbcda0 100644 --- a/django/views/generic/list.py +++ b/django/views/generic/list.py @@ -97,7 +97,7 @@ class MultipleObjectMixin(ContextMixin): if self.context_object_name: return self.context_object_name elif hasattr(object_list, 'model'): - return '%s_list' % object_list.model._meta.object_name.lower() + return '%s_list' % object_list.model._meta.model_name else: return None @@ -177,7 +177,7 @@ class MultipleObjectTemplateResponseMixin(TemplateResponseMixin): # generated ones. if hasattr(self.object_list, 'model'): opts = self.object_list.model._meta - names.append("%s/%s%s.html" % (opts.app_label, opts.object_name.lower(), self.template_name_suffix)) + names.append("%s/%s%s.html" % (opts.app_label, opts.model_name, self.template_name_suffix)) return names diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index df3d84fdae..50b9aa3c19 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -327,6 +327,8 @@ these changes. :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to your :setting:`MIDDLEWARE_CLASSES` setting instead. +* ``Model._meta.module_name`` was renamed to ``model_name``. + 2.0 --- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 32e5172878..f86d8b8108 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -127,3 +127,9 @@ from your settings. If you defined your own form widgets and defined the ``_has_changed`` method on a widget, you should now define this method on the form field itself. + +``module_name`` model meta attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Model._meta.module_name`` was renamed to ``model_name``. Despite being a +private API, it will go through a regular deprecation path. diff --git a/tests/regressiontests/admin_custom_urls/models.py b/tests/regressiontests/admin_custom_urls/models.py index ef04c2aa09..55fc064835 100644 --- a/tests/regressiontests/admin_custom_urls/models.py +++ b/tests/regressiontests/admin_custom_urls/models.py @@ -42,7 +42,7 @@ class ActionAdmin(admin.ModelAdmin): return self.admin_site.admin_view(view)(*args, **kwargs) return update_wrapper(wrapper, view) - info = self.model._meta.app_label, self.model._meta.module_name + info = self.model._meta.app_label, self.model._meta.model_name view_name = '%s_%s_add' % info -- cgit v1.3 From f49e9a517f2fdc1d9ed7ac841ace77636cbd6747 Mon Sep 17 00:00:00 2001 From: Vladimir A Filonov Date: Sat, 23 Feb 2013 15:07:21 +0100 Subject: Fixed #17906 - Autoescaping {% cycle %} and {% firstof %} templatetags. This commit adds "future" version of these two tags with auto-escaping enabled. --- django/template/defaulttags.py | 48 ++++++++++++++++++--------- django/templatetags/future.py | 57 ++++++++++++++++++++++++++++++-- docs/internals/deprecation.txt | 4 +++ docs/ref/templates/builtins.txt | 55 +++++++++++++++++++++++------- docs/releases/1.6.txt | 28 ++++++++++++++++ tests/regressiontests/templates/tests.py | 12 ++++++- 6 files changed, 173 insertions(+), 31 deletions(-) (limited to 'docs/internals') diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index c1ca947753..41db90c0ac 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -5,13 +5,15 @@ import sys import re from datetime import datetime from itertools import groupby, cycle as itertools_cycle +import warnings from django.conf import settings from django.template.base import (Node, NodeList, Template, Context, Library, TemplateSyntaxError, VariableDoesNotExist, InvalidTemplateLibrary, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, SINGLE_BRACE_START, SINGLE_BRACE_END, COMMENT_TAG_START, COMMENT_TAG_END, - VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re) + VARIABLE_ATTRIBUTE_SEPARATOR, get_library, token_kwargs, kwarg_re, + _render_value_in_context) from django.template.smartif import IfParser, Literal from django.template.defaultfilters import date from django.utils.encoding import smart_text @@ -54,15 +56,15 @@ class CsrfTokenNode(Node): # misconfiguration, so we raise a warning from django.conf import settings if settings.DEBUG: - import warnings warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.") return '' class CycleNode(Node): - def __init__(self, cyclevars, variable_name=None, silent=False): + def __init__(self, cyclevars, variable_name=None, silent=False, escape=False): self.cyclevars = cyclevars self.variable_name = variable_name self.silent = silent + self.escape = escape # only while the "future" version exists def render(self, context): if self not in context.render_context: @@ -74,7 +76,9 @@ class CycleNode(Node): context[self.variable_name] = value if self.silent: return '' - return value + if not self.escape: + value = mark_safe(value) + return _render_value_in_context(value, context) class DebugNode(Node): def render(self, context): @@ -97,14 +101,17 @@ class FilterNode(Node): return filtered class FirstOfNode(Node): - def __init__(self, vars): - self.vars = vars + def __init__(self, variables, escape=False): + self.vars = variables + self.escape = escape # only while the "future" version exists def render(self, context): for var in self.vars: value = var.resolve(context, True) if value: - return smart_text(value) + if not self.escape: + value = mark_safe(value) + return _render_value_in_context(value, context) return '' class ForNode(Node): @@ -508,7 +515,7 @@ def comment(parser, token): return CommentNode() @register.tag -def cycle(parser, token): +def cycle(parser, token, escape=False): """ Cycles among the given strings each time this tag is encountered. @@ -541,6 +548,11 @@ def cycle(parser, token): {% endfor %} """ + if not escape: + warnings.warn( + "'The syntax for the `cycle` template tag is changing. Load it " + "from the `future` tag library to start using the new behavior.", + PendingDeprecationWarning, stacklevel=2) # Note: This returns the exact same node on each {% cycle name %} call; # that is, the node object returned from {% cycle a b c as name %} and the @@ -588,13 +600,13 @@ def cycle(parser, token): if as_form: name = args[-1] values = [parser.compile_filter(arg) for arg in args[1:-2]] - node = CycleNode(values, name, silent=silent) + node = CycleNode(values, name, silent=silent, escape=escape) if not hasattr(parser, '_namedCycleNodes'): parser._namedCycleNodes = {} parser._namedCycleNodes[name] = node else: values = [parser.compile_filter(arg) for arg in args[1:]] - node = CycleNode(values) + node = CycleNode(values, escape=escape) return node @register.tag @@ -643,7 +655,7 @@ def do_filter(parser, token): return FilterNode(filter_expr, nodelist) @register.tag -def firstof(parser, token): +def firstof(parser, token, escape=False): """ Outputs the first variable passed that is not False, without escaping. @@ -657,11 +669,11 @@ def firstof(parser, token): {% if var1 %} {{ var1|safe }} - {% else %}{% if var2 %} + {% elif var2 %} {{ var2|safe }} - {% else %}{% if var3 %} + {% elif var3 %} {{ var3|safe }} - {% endif %}{% endif %}{% endif %} + {% endif %} but obviously much cleaner! @@ -677,10 +689,16 @@ def firstof(parser, token): {% endfilter %} """ + if not escape: + warnings.warn( + "'The syntax for the `firstof` template tag is changing. Load it " + "from the `future` tag library to start using the new behavior.", + PendingDeprecationWarning, stacklevel=2) + bits = token.split_contents()[1:] if len(bits) < 1: raise TemplateSyntaxError("'firstof' statement requires at least one argument") - return FirstOfNode([parser.compile_filter(bit) for bit in bits]) + return FirstOfNode([parser.compile_filter(bit) for bit in bits], escape=escape) @register.tag('for') def do_for(parser, token): diff --git a/django/templatetags/future.py b/django/templatetags/future.py index e6a0127e71..a385c6d565 100644 --- a/django/templatetags/future.py +++ b/django/templatetags/future.py @@ -1,14 +1,65 @@ from django.template import Library -from django.template.defaulttags import url as default_url, ssi as default_ssi +from django.template import defaulttags register = Library() + @register.tag def ssi(parser, token): # Used for deprecation path during 1.3/1.4, will be removed in 2.0 - return default_ssi(parser, token) + return defaulttags.ssi(parser, token) + @register.tag def url(parser, token): # Used for deprecation path during 1.3/1.4, will be removed in 2.0 - return default_url(parser, token) + return defaulttags.url(parser, token) + + +@register.tag +def cycle(parser, token): + """ + This is the future version of `cycle` with auto-escaping. + + By default all strings are escaped. + + If you want to disable auto-escaping of variables you can use: + + {% autoescape off %} + {% cycle var1 var2 var3 as somecycle %} + {% autoescape %} + + Or if only some variables should be escaped, you can use: + + {% cycle var1 var2|safe var3|safe as somecycle %} + """ + return defaulttags.cycle(parser, token, escape=True) + + +@register.tag +def firstof(parser, token): + """ + This is the future version of `firstof` with auto-escaping. + + This is equivalent to: + + {% if var1 %} + {{ var1 }} + {% elif var2 %} + {{ var2 }} + {% elif var3 %} + {{ var3 }} + {% endif %} + + If you want to disable auto-escaping of variables you can use: + + {% autoescape off %} + {% firstof var1 var2 var3 "fallback value" %} + {% autoescape %} + + Or if only some variables should be escaped, you can use: + + {% firstof var1 var2|safe var3 "fallback value"|safe %} + + """ + return defaulttags.firstof(parser, token, escape=True) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 50b9aa3c19..ef9fd31d15 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -323,6 +323,10 @@ these changes. 1.8 --- +* The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their + arguments. In 1.6 and 1.7, this behavior is provided by the version of these + tags in the ``future`` template tag library. + * The ``SEND_BROKEN_LINK_EMAILS`` setting will be removed. Add the :class:`django.middleware.common.BrokenLinkEmailsMiddleware` middleware to your :setting:`MIDDLEWARE_CLASSES` setting instead. diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index cfc57cc551..149a557356 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -147,9 +147,8 @@ You can use any number of values in a ``{% cycle %}`` tag, separated by spaces. Values enclosed in single (``'``) or double quotes (``"``) are treated as string literals, while values without quotes are treated as template variables. -Note that the variables included in the cycle will not be escaped. -This is because template tags do not escape their content. Any HTML or -Javascript code contained in the printed variable will be rendered +Note that currently the variables included in the cycle will not be escaped. +Any HTML or Javascript code contained in the printed variable will be rendered as-is, which could potentially lead to security issues. For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior @@ -190,6 +189,22 @@ call to ``{% cycle %}`` doesn't specify silent:: {% cycle 'row1' 'row2' as rowcolors silent %} {% cycle rowcolors %} +.. versionchanged:: 1.6 + +To improve safety, future versions of ``cycle`` will automatically escape +their output. You're encouraged to activate this behavior by loading +``cycle`` from the ``future`` template library:: + + {% load cycle from future %} + +When using the ``future`` version, you can disable auto-escaping with:: + + {% for o in some_list %} + + ... + + {% endfor %} + .. templatetag:: debug debug @@ -257,28 +272,44 @@ This is equivalent to:: {% if var1 %} {{ var1|safe }} - {% else %}{% if var2 %} + {% elif var2 %} {{ var2|safe }} - {% else %}{% if var3 %} + {% elif var3 %} {{ var3|safe }} - {% endif %}{% endif %}{% endif %} + {% endif %} You can also use a literal string as a fallback value in case all passed variables are False:: {% firstof var1 var2 var3 "fallback value" %} -Note that the variables included in the firstof tag will not be -escaped. This is because template tags do not escape their content. -Any HTML or Javascript code contained in the printed variable will be -rendered as-is, which could potentially lead to security issues. If you -need to escape the variables in the firstof tag, you must do so -explicitly:: +Note that currently the variables included in the firstof tag will not be +escaped. Any HTML or Javascript code contained in the printed variable will be +rendered as-is, which could potentially lead to security issues. If you need +to escape the variables in the firstof tag, you must do so explicitly:: {% filter force_escape %} {% firstof var1 var2 var3 "fallback value" %} {% endfilter %} +.. versionchanged:: 1.6 + +To improve safety, future versions of ``firstof`` will automatically escape +their output. You're encouraged to activate this behavior by loading +``firstof`` from the ``future`` template library:: + + {% load firstof from future %} + +When using the ``future`` version, you can disable auto-escaping with:: + + {% autoescape off %} + {% firstof var1 var2 var3 "fallback value" %} + {% endautoescape %} + +Or if only some variables should be escaped, you can use:: + + {% firstof var1 var2|safe var3 "fallback value"|safe %} + .. templatetag:: for for diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 67c032a362..ce1e643946 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -160,6 +160,34 @@ Backwards incompatible changes in 1.6 Features deprecated in 1.6 ========================== +Changes to :ttag:`cycle` and :ttag:`firstof` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The template system generally escapes all variables to avoid XSS attacks. +However, due to an accident of history, the :ttag:`cycle` and :ttag:`firstof` +tags render their arguments as-is. + +Django 1.6 starts a process to correct this inconsistency. The ``future`` +template library provides alternate implementations of :ttag:`cycle` and +:ttag:`firstof` that autoescape their inputs. If you're using these tags, +you're encourage to include the following line at the top of your templates to +enable the new behavior:: + + {% load cycle from future %} + +or:: + + {% load firstof from future %} + +The tags implementing the old behavior have been deprecated, and in Django +1.8, the old behavior will be replaced with the new behavior. To ensure +compatibility with future versions of Django, existing templates should be +modified to use the ``future`` versions. + +If necessary, you can temporarily disable auto-escaping with +:func:`~django.utils.safestring.mark_safe` or :ttag:`{% autoescape off %} +`. + ``SEND_BROKEN_LINK_EMAILS`` setting ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index 02d3460b72..95b090fcde 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -773,6 +773,11 @@ class Templates(TestCase): 'cycle23': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{{ abc }}{{ x }}{% endfor %}", {'values': [1,2,3,4]}, "a1b2c3a4"), 'included-cycle': ('{{ abc }}', {'abc': 'xxx'}, 'xxx'), 'cycle24': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{% include 'included-cycle' %}{% endfor %}", {'values': [1,2,3,4]}, "abca"), + 'cycle25': ('{% cycle a as abc %}', {'a': '<'}, '<'), + + 'cycle26': ('{% load cycle from future %}{% cycle a b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '<>'), + 'cycle27': ('{% load cycle from future %}{% autoescape off %}{% cycle a b as ab %}{% cycle ab %}{% endautoescape %}', {'a': '<', 'b': '>'}, '<>'), + 'cycle28': ('{% load cycle from future %}{% cycle a|safe b as ab %}{% cycle ab %}', {'a': '<', 'b': '>'}, '<>'), ### EXCEPTIONS ############################################################ @@ -804,7 +809,12 @@ class Templates(TestCase): 'firstof07': ('{% firstof a b "c" %}', {'a':0}, 'c'), 'firstof08': ('{% firstof a b "c and d" %}', {'a':0,'b':0}, 'c and d'), 'firstof09': ('{% firstof %}', {}, template.TemplateSyntaxError), - 'firstof10': ('{% firstof a %}', {'a': '<'}, '<'), # Variables are NOT auto-escaped. + 'firstof10': ('{% firstof a %}', {'a': '<'}, '<'), + + 'firstof11': ('{% load firstof from future %}{% firstof a b %}', {'a': '<', 'b': '>'}, '<'), + 'firstof12': ('{% load firstof from future %}{% firstof a b %}', {'a': '', 'b': '>'}, '>'), + 'firstof13': ('{% load firstof from future %}{% autoescape off %}{% firstof a %}{% endautoescape %}', {'a': '<'}, '<'), + 'firstof14': ('{% load firstof from future %}{% firstof a|safe b %}', {'a': '<'}, '<'), ### FOR TAG ############################################################### 'for-tag01': ("{% for val in values %}{{ val }}{% endfor %}", {"values": [1, 2, 3]}, "123"), -- cgit v1.3 From 1cd2f51eb43f9ed043982770b4efd5f28f53f302 Mon Sep 17 00:00:00 2001 From: Zbigniew Siciarz Date: Sat, 23 Feb 2013 17:10:48 +0100 Subject: Added test runner option to skip Selenium tests (#19854). --- django/contrib/admin/tests.py | 4 ++++ docs/internals/contributing/writing-code/unit-tests.txt | 9 +++++++++ tests/regressiontests/views/tests/i18n.py | 5 +++++ tests/runtests.py | 10 +++++++++- 4 files changed, 27 insertions(+), 1 deletion(-) (limited to 'docs/internals') diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index c99488cd41..30d63e4486 100644 --- a/django/contrib/admin/tests.py +++ b/django/contrib/admin/tests.py @@ -1,3 +1,5 @@ +import os + from django.test import LiveServerTestCase from django.utils.module_loading import import_by_path from django.utils.unittest import SkipTest @@ -8,6 +10,8 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase): @classmethod def setUpClass(cls): + if os.environ.get('DJANGO_SKIP_SELENIUM_TESTS', False): + raise SkipTest('Selenium tests skipped by explicit request') try: cls.selenium = import_by_path(cls.webdriver_class)() except Exception as e: diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index a03951d141..59f4c97c92 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -136,6 +136,15 @@ Then, run the tests normally, for example: ./runtests.py --settings=test_sqlite admin_inlines +If you have Selenium installed but for some reason don't want to run these tests +(for example to speed up the test suite), use the ``--skip-selenium`` option +of the test runner. + +.. code-block:: bash + + ./runtests.py --settings=test_sqlite --skip-selenium admin_inlines + + .. _running-unit-tests-dependencies: Running all the tests diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py index 0a091ed1b7..206cf3d256 100644 --- a/tests/regressiontests/views/tests/i18n.py +++ b/tests/regressiontests/views/tests/i18n.py @@ -2,6 +2,7 @@ from __future__ import absolute_import import gettext +import os from os import path from django.conf import settings @@ -176,6 +177,10 @@ class JsI18NTestsMultiPackage(TestCase): javascript_quote('este texto de app3 debe ser traducido')) +skip_selenium = os.environ.get('DJANGO_SKIP_SELENIUM_TESTS', False) + + +@unittest.skipIf(skip_selenium, 'Selenium tests skipped by explicit request') @unittest.skipUnless(firefox, 'Selenium not installed') class JavascriptI18nTests(LiveServerTestCase): urls = 'regressiontests.views.urls' diff --git a/tests/runtests.py b/tests/runtests.py index c23737ed14..b9de137ea2 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -301,7 +301,12 @@ if __name__ == "__main__": '--liveserver', action='store', dest='liveserver', default=None, help='Overrides the default address where the live server (used with ' 'LiveServerTestCase) is expected to run from. The default value ' - 'is localhost:8081.'), + 'is localhost:8081.') + parser.add_option( + '--skip-selenium', action='store_true', dest='skip_selenium', + default=False, + help='Skip running Selenium tests even it Selenium itself is ' + 'installed. By default these tests are not skipped.') options, args = parser.parse_args() if options.settings: os.environ['DJANGO_SETTINGS_MODULE'] = options.settings @@ -314,6 +319,9 @@ if __name__ == "__main__": if options.liveserver is not None: os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options.liveserver + if options.skip_selenium: + os.environ['DJANGO_SKIP_SELENIUM_TESTS'] = '1' + if options.bisect: bisect_tests(options.bisect, options, args) elif options.pair: -- cgit v1.3 From e3296268de8f2c387c4155ae1b8d39bb109f265d Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sat, 23 Feb 2013 12:33:57 -0600 Subject: Added a draft document explaining how to release Django. Thanks to James for the first draft; I made a few changes (svn->git) and some supporting links, but mostly I added FIXME's. --- docs/internals/howto-release-django.txt | 280 ++++++++++++++++++++++++++++++++ docs/internals/index.txt | 1 + 2 files changed, 281 insertions(+) create mode 100644 docs/internals/howto-release-django.txt (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt new file mode 100644 index 0000000000..109583b67f --- /dev/null +++ b/docs/internals/howto-release-django.txt @@ -0,0 +1,280 @@ +===================== +How is Django Formed? +===================== + +This document explains how to release Django. If you're unluky enough to +be driving a release, you should follow these instructions to get the +package out. + +**Please, keep these instructions up-to-date if you make changes!** The point +here is to be descriptive, not proscriptive, so feel free to streamline or +otherwise make changes, but **update this document accordingly!** + +Overview +======== + +There are three types of releases that you might need to make + +* Security releases, disclosing and fixing a vulnerability. This'll + generally involve two or three simultaneous releases -- e.g. + 1.5.X, 1.6.X, and, depending on timing, perhaps a 1.7 alpha/beta/rc. + +* Regular version releases, either a final release (e.g. 1.5) or a + bugfix update (e.g. 1.5.1). + +* Pre-releases, e.g. 1.6 beta or something. + +In general the steps are about the same reguardless, but there are a few +differences noted. The short version is: + +#. If this is a security release, pre-notify the security distribution list + at least one week before the actual release. + +#. Proofread (and create if needed) the release notes, looking for + organiztion, writing errors, deprecation timelines, etc. Draft a blog post + and email announcement. + +#. Update version numbers and create the release package(s)! + +#. Upload the package(s) to the the ``djangoproject.com`` server and creating + some redirects for download/checksum links. + +#. Unless this is a pre-release, add the new version(s) to PyPI. + +#. Update the home page and download page to link to the new version(s). + +#. Post the blog entry and send out the email announcements. + +#. Update version numbers post-release. + +There's a lot of details, so please read on. + +Prerequisites +============= + +You'll need a few things hooked up to make this work: + +* A GPG key. *FIXME: sort out exactly whose keys are acceptable for a + release.* + +* Access to Django's record on PyPI. + +* Access to the ``djangoproject.com`` server to upload files and trigger a + deploy. + +* Access to the admin on ``djangoproject.com``. + +* Access to post to ``django-announe``. + +* If this is a security release, access to the pre-notification distribution + list. + +If this is your first release, you'll need to corrdinate with James and Jacob +to get all these things ready to go. + +Pre-release tasks +================= + +A few items need to be taken care of before even beginning the release process. +This stuff starts about a week before the release; most of it can be done +any time leading up to the actual release: + +#. If this is a security release, send out pre-notification **one week** + before the release. We maintain a list of who gets these pre-notifcation + emails at *FIXME WHERE?*. This email should be signed by the key you'll use + for the release, and should include patches for each issue being fixed. + +#. As the release aproaches, watch Trac to make sure no release blockers + are left for the upcoming release. + +#. Check with the other committers to make sure they don't have any + un-committed changes for the release. + +#. Proofread the release notes, including looking at the online + version to catch any broken links or reST errors, and make sure the + release notes contain the correct date. + +#. Double-check that the release notes mention deprecation timelines + for any APIs noted as deprecated, and that they mention any changes + in Python version support. + +#. Double-check that the release notes index has a link to the notes + for the new release; this will be in ``docs/releases/index.txt``. + +Preparing for release +===================== + +Next, everything needs to be made ready for actually rolling the +release. The following things should be done a few days to a few hours +before release: + +#. Update the djangoproject home page and download page templates to + reflect the new release. There are two templates to change: + ``flatpages/download.html`` and ``homepage.html``; here's + `one example commit for the 1.4.5 / 1.3.7 releases`__ + + __ https://github.com/django/djangoproject.com/commit/772edbc6ac5a2b8e718606b3338f2bcc429fb9b6 + +#. Write the announcement blog post for the release. You can enter it into + the admin at any time and mark it as inactive. Here's a few examples: + `example security release accouncement`__, `example regular release + announcement`__, `example pre-release announcement`__. + + __ https://www.djangoproject.com/weblog/2013/feb/19/security/ + __ https://www.djangoproject.com/weblog/2012/mar/23/14/ + __ https://www.djangoproject.com/weblog/2012/nov/27/15-beta-1/ + +#. Create redirects in the admin for the new downloads. For each release, + we create two redirects that look like:: + + /download//tarball/ -> /m/releases//Django-.tar.gz + /download//checksum/ -> /m/pgp/Django-.checksum.txt + +Actually rolling the release +============================ + +OK, this is the fun part, where we actually push out a release! + +#. Check Jenkins is green for the version(s) you're putting out. You probably + shouldn't issue a release until it's green. + +#. A release always begins from a release branch, so you + should ``git pull`` to make sure you're up-to-date and then + ``git checkout stable/`` (e.g. checkout ``stable/1.5.x`` to issue + a release in the 1.5 series.) + +#. If this is a security release, merge the apropriate patches from + ``django-private``. *FIXME: actual commands here - make sure to --ff- + only right?*. Make sure the commit messages explain that the commit + is a security fix and that an announcement will follow (`example + security commit`__) + + __ https://github.com/django/django/commit/3ef4bbf495cc6c061789132e3d50a8231a89406b + +#. Update version numbers for the release. This has to happen in three + places: ``django/__init__.py``, ``docs/conf.py``, and ``setup.py``. + Please see `notes on setting the VERSION tuple`_ below for details + on ``VERSION``. Here's `an example commit updating version numbers`__ + + __ https://github.com/django/django/commit/18d920ea4839fb54f9d2a5dcb555b6a5666ee469 + + Make sure the ``download_url`` in ``setup.py`` is the actual URL you'll + use for the new release package, not the redirect URL (some tools can't + properly follow redirects). + +#. If this is a pre-release package, update the "Development Status" trove + classifier in ``setup.py`` to reflect this. Otherwise, make sure the + classifier is set to ``Development Status :: 5 - Production/Stable``. + +#. Tag the release by running ``git tag`` *FIXME actual commands*. + +#. ``git push`` your work. + +#. Make sure you have an absolutely clean tree by running ``git clean -dfx``. + +#. Run ``python setup.py sdist`` to generate the release package. + +#. Generate the MD5 and SHA1 hashes of the release package. *FIXME + actual commands for doign this?* + +#. Create a "checksums" file containing the hashes and release information. + You can start with `a previous checksums file`__ and replace the + dates, keys, links, and checksums. *FIXME: make a template file.* + + __ https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt + +#. Sign the checksum file using the release key (``gpg + --clearsign``), then verify the signature (``gpg --verify``). *FIXME: + full, actual commands here*. + +If you're issuing multiple releases, repeat these steps for each release. + +Making the release(s) available to the public +============================================= + +Now you're ready to actually put the release out there. To do this: + +#. Upload the release package(s) to the djangoproject server; releases go + in ``/home/www/djangoproject.com/src/media/releases``, under a + directory for the appropriate version number (e.g. + ``/home/www/djangoproject.com/src/media/releases/1.5`` for a ``1.5.X`` + release.). + +#. Upload the checksum file(s); these go in + ``/home/www/djangoproject.com/src/media/pgp``. + +#. Test that the release packages install correctly using ``easy_install`` + and ``pip``. Here's how I do it (which requires `virtualenvwrapper`__): + + $ mktmpenv + $ easy_install http://www.djangoproject.com/download//tarball/ + $ deactivate + $ mktmpenv + $ pip install http://www.djangoproject.com/download//tarball/ + $ deactivate + + This just tests that the tarballs are available (i.e. redirects are up) and + that they install correctly, but it'll catch silly mistakes. *XXX FIXME: + buildout too?* + + __ https://pypi.python.org/pypi/virtualenvwrapper + +#. Ask a few people on IRC to verify the checksums by visiting the chucksums + file (e.g. https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt) + and following the instructions in it. + +#. If this is a security or regular release, register the new package with + PyPI by uploading the ``PGK-INFO`` file generated in the release package + *FIXME: be more specific about where this is and how to upload it.* + Don't do this for pre-releases. + +#. Deploy the template changes you made a while back by running `fab deploy` + from the ``djangoproject.com`` repo. + +#. Update the ``/download/`` flat page in the djangoproject.com + admin. For alpha/beta/RC releases, we add a temporary third section + to that page listing the preview package; otherwise, just update + the "Get the latest official version" section. + +#. Make up the blog post announcing the release live. + +#. Post the release announcement to the django-announce, + django-developers and django-users mailing lists. This should + include links to both the announcement blog post and the release + notes. *FIXME: make some templates with example text*. + +Post-release +============ + +You're almost done! All that's left to do now is: + +#. Update the ``VERSION`` tuple in ``django/__init__.py`` again, + incrementing to whatever the next expected release will be. For + example, after releasing 1.2.1, update ``VERSION`` to report "1.2.2 + pre-alpha". + +Notes on setting the VERSION tuple +================================== + +Django's version reporting is controlled by the ``VERSION`` tuple in +``django/__init__.py``. This is a five-element tuple, whose elements +are: + +#. Major version. +#. Minor version. +#. Micro version. +#. Status -- can be one of "alpha", "beta", "rc" or "final". +#. Series number, for alpha/beta/RC packages which run in sequence + (allowing, for example, "beta 1", "beta 2", etc.). + +For a final release, the status is always "final" and the series +number is always 0. A series number of 0 with an "alpha" status will +be reported as "pre-alpha". + +Some examples: + +* ``(1, 2, 1, 'final', 0)`` --> "1.2.1" + +* ``(1, 3, 0, 'alpha', 0)`` --> "1.3 pre-alpha" + +* ``(1, 3, 0, 'beta', 2)`` --> "1.3 beta 2" diff --git a/docs/internals/index.txt b/docs/internals/index.txt index 3ff4eb62d0..9a80a90286 100644 --- a/docs/internals/index.txt +++ b/docs/internals/index.txt @@ -22,3 +22,4 @@ the hood". release-process deprecation git + howto-release-django -- cgit v1.3 From 799be90fde8a7b77b3876eb593751e410c718d1f Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sat, 23 Feb 2013 13:01:52 -0600 Subject: Some updates to "how to release Django": Typo fixes, spell check, some more specifics where possible. --- docs/internals/howto-release-django.txt | 40 ++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 109583b67f..1dc80b553e 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -2,7 +2,7 @@ How is Django Formed? ===================== -This document explains how to release Django. If you're unluky enough to +This document explains how to release Django. If you're unlucky enough to be driving a release, you should follow these instructions to get the package out. @@ -24,14 +24,14 @@ There are three types of releases that you might need to make * Pre-releases, e.g. 1.6 beta or something. -In general the steps are about the same reguardless, but there are a few +In general the steps are about the same regardless, but there are a few differences noted. The short version is: #. If this is a security release, pre-notify the security distribution list at least one week before the actual release. #. Proofread (and create if needed) the release notes, looking for - organiztion, writing errors, deprecation timelines, etc. Draft a blog post + organization, writing errors, deprecation timelines, etc. Draft a blog post and email announcement. #. Update version numbers and create the release package(s)! @@ -64,12 +64,12 @@ You'll need a few things hooked up to make this work: * Access to the admin on ``djangoproject.com``. -* Access to post to ``django-announe``. +* Access to post to ``django-announce``. * If this is a security release, access to the pre-notification distribution list. -If this is your first release, you'll need to corrdinate with James and Jacob +If this is your first release, you'll need to coordinate with James and Jacob to get all these things ready to go. Pre-release tasks @@ -80,11 +80,11 @@ This stuff starts about a week before the release; most of it can be done any time leading up to the actual release: #. If this is a security release, send out pre-notification **one week** - before the release. We maintain a list of who gets these pre-notifcation + before the release. We maintain a list of who gets these pre-notification emails at *FIXME WHERE?*. This email should be signed by the key you'll use for the release, and should include patches for each issue being fixed. -#. As the release aproaches, watch Trac to make sure no release blockers +#. As the release approaches, watch Trac to make sure no release blockers are left for the upcoming release. #. Check with the other committers to make sure they don't have any @@ -117,7 +117,7 @@ before release: #. Write the announcement blog post for the release. You can enter it into the admin at any time and mark it as inactive. Here's a few examples: - `example security release accouncement`__, `example regular release + `example security release announcement`__, `example regular release announcement`__, `example pre-release announcement`__. __ https://www.djangoproject.com/weblog/2013/feb/19/security/ @@ -143,7 +143,7 @@ OK, this is the fun part, where we actually push out a release! ``git checkout stable/`` (e.g. checkout ``stable/1.5.x`` to issue a release in the 1.5 series.) -#. If this is a security release, merge the apropriate patches from +#. If this is a security release, merge the appropriate patches from ``django-private``. *FIXME: actual commands here - make sure to --ff- only right?*. Make sure the commit messages explain that the commit is a security fix and that an announcement will follow (`example @@ -172,10 +172,13 @@ OK, this is the fun part, where we actually push out a release! #. Make sure you have an absolutely clean tree by running ``git clean -dfx``. -#. Run ``python setup.py sdist`` to generate the release package. +#. Run ``python setup.py sdist`` to generate the release package. This will + create the release package in a ``dist/`` directory. -#. Generate the MD5 and SHA1 hashes of the release package. *FIXME - actual commands for doign this?* +#. Generate the MD5 and SHA1 hashes of the release package:: + + $ md5sum dist/Django-.tar.gz + $ sha1sum dist/Django-.tar.gz #. Create a "checksums" file containing the hashes and release information. You can start with `a previous checksums file`__ and replace the @@ -207,10 +210,10 @@ Now you're ready to actually put the release out there. To do this: and ``pip``. Here's how I do it (which requires `virtualenvwrapper`__): $ mktmpenv - $ easy_install http://www.djangoproject.com/download//tarball/ + $ easy_install https://www.djangoproject.com/download//tarball/ $ deactivate $ mktmpenv - $ pip install http://www.djangoproject.com/download//tarball/ + $ pip install https://www.djangoproject.com/download//tarball/ $ deactivate This just tests that the tarballs are available (i.e. redirects are up) and @@ -224,9 +227,10 @@ Now you're ready to actually put the release out there. To do this: and following the instructions in it. #. If this is a security or regular release, register the new package with - PyPI by uploading the ``PGK-INFO`` file generated in the release package - *FIXME: be more specific about where this is and how to upload it.* - Don't do this for pre-releases. + PyPI by uploading the ``PGK-INFO`` file generated in the release package. + This file's *in* the distribution tarball, so you'll need to pull it + out. ``tar xzf dist/Django-.tar.gz Django-/PKG-INFO`` + ought to work. #. Deploy the template changes you made a while back by running `fab deploy` from the ``djangoproject.com`` repo. @@ -251,7 +255,7 @@ You're almost done! All that's left to do now is: #. Update the ``VERSION`` tuple in ``django/__init__.py`` again, incrementing to whatever the next expected release will be. For example, after releasing 1.2.1, update ``VERSION`` to report "1.2.2 - pre-alpha". + pre-alpha". *FIXME: Is this correct? Do we still do this?* Notes on setting the VERSION tuple ================================== -- cgit v1.3 From 9d2c0a0ae6ce931699daa87735d5b8b2afaa20f9 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Sat, 23 Feb 2013 14:19:01 -0800 Subject: Removed superfluous cookie check from auth login. This is ensured through the CSRF protection of the view --- django/contrib/admin/forms.py | 1 - django/contrib/auth/forms.py | 9 ++++----- django/contrib/auth/views.py | 5 ----- docs/internals/deprecation.txt | 6 ++++++ 4 files changed, 10 insertions(+), 11 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/admin/forms.py b/django/contrib/admin/forms.py index 1fabdce245..38c445f71a 100644 --- a/django/contrib/admin/forms.py +++ b/django/contrib/admin/forms.py @@ -33,5 +33,4 @@ class AdminAuthenticationForm(AuthenticationForm): raise forms.ValidationError(message % { 'username': self.username_field.verbose_name }) - self.check_for_test_cookie() return self.cleaned_data diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index c28971b94d..f3ad655c65 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import warnings + from django import forms from django.forms.util import flatatt from django.template import loader @@ -153,8 +155,6 @@ class AuthenticationForm(forms.Form): error_messages = { 'invalid_login': _("Please enter a correct %(username)s and password. " "Note that both fields may be case-sensitive."), - 'no_cookies': _("Your Web browser doesn't appear to have cookies " - "enabled. Cookies are required for logging in."), 'inactive': _("This account is inactive."), } @@ -189,12 +189,11 @@ class AuthenticationForm(forms.Form): }) elif not self.user_cache.is_active: raise forms.ValidationError(self.error_messages['inactive']) - self.check_for_test_cookie() return self.cleaned_data def check_for_test_cookie(self): - if self.request and not self.request.session.test_cookie_worked(): - raise forms.ValidationError(self.error_messages['no_cookies']) + warnings.warn("check_for_test_cookie is deprecated; ensure your login " + "view is CSRF-protected.", DeprecationWarning) def get_user_id(self): if self.user_cache: diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 9d1534651b..c9f53f1956 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -45,15 +45,10 @@ def login(request, template_name='registration/login.html', # Okay, security check complete. Log the user in. auth_login(request, form.get_user()) - if request.session.test_cookie_worked(): - request.session.delete_test_cookie() - return HttpResponseRedirect(redirect_to) else: form = authentication_form(request) - request.session.set_test_cookie() - current_site = get_current_site(request) context = { diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index ef9fd31d15..f1ae1338df 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -320,6 +320,12 @@ these changes. deprecated. Use the :class:`warnings.catch_warnings` context manager available starting with Python 2.6 instead. +* The undocumented ``check_for_test_cookie`` method in + :class:`~django.contrib.auth.forms.AuthenticationForm` will be removed + following an accelerated deprecation. Users subclassing this form should + remove calls to this method, and instead ensure that their auth related views + are CSRF protected, which ensures that cookies are enabled. + 1.8 --- -- cgit v1.3 From f480b395256612de83b2f912bfecee03366bc990 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Sat, 23 Feb 2013 17:58:57 -0700 Subject: Various tweaks and additions to 'how to release Django' document. --- docs/internals/howto-release-django.txt | 63 ++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 25 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 1dc80b553e..fffdc9b869 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -36,7 +36,7 @@ differences noted. The short version is: #. Update version numbers and create the release package(s)! -#. Upload the package(s) to the the ``djangoproject.com`` server and creating +#. Upload the package(s) to the the ``djangoproject.com`` server and create some redirects for download/checksum links. #. Unless this is a pre-release, add the new version(s) to PyPI. @@ -47,7 +47,7 @@ differences noted. The short version is: #. Update version numbers post-release. -There's a lot of details, so please read on. +There are a lot of details, so please read on. Prerequisites ============= @@ -116,7 +116,7 @@ before release: __ https://github.com/django/djangoproject.com/commit/772edbc6ac5a2b8e718606b3338f2bcc429fb9b6 #. Write the announcement blog post for the release. You can enter it into - the admin at any time and mark it as inactive. Here's a few examples: + the admin at any time and mark it as inactive. Here are a few examples: `example security release announcement`__, `example regular release announcement`__, `example pre-release announcement`__. @@ -135,19 +135,28 @@ Actually rolling the release OK, this is the fun part, where we actually push out a release! -#. Check Jenkins is green for the version(s) you're putting out. You probably - shouldn't issue a release until it's green. +#. Check `Jenkins`__ is green for the version(s) you're putting out. You + probably shouldn't issue a release until it's green. + + __ http://ci.djangoproject.com + +#. A release always begins from a release branch, so you should ``git checkout + stable/`` (e.g. checkout ``stable/1.5.x`` to issue a release in the + 1.5 series) and then ``git pull`` to make sure you're up-to-date. -#. A release always begins from a release branch, so you - should ``git pull`` to make sure you're up-to-date and then - ``git checkout stable/`` (e.g. checkout ``stable/1.5.x`` to issue - a release in the 1.5 series.) #. If this is a security release, merge the appropriate patches from - ``django-private``. *FIXME: actual commands here - make sure to --ff- - only right?*. Make sure the commit messages explain that the commit - is a security fix and that an announcement will follow (`example - security commit`__) + ``django-private``. Rebase these patches as necessary to make each one a + simple commit on the release branch rather than a merge commit. To ensure + this, merge them with the ``--ff-only`` flag; for example, ``git checkout + stable/1.5.x; git merge --ff-only security/1.5.x``, if ``security/1.5.x`` is + a branch in the ``django-private`` repo containing the necessary security + patches for the next release in the 1.5 series. If git refuses to merge with + ``--ff-only``, switch to the security-patch branch and rebase it on the + branch you are about to merge it into (``git checkout security/1.5.x; git + rebase stable/1.5.x``) and then switch back and do the merge. Make sure the + commit message for each security fix explains that the commit is a security + fix and that an announcement will follow (`example security commit`__) __ https://github.com/django/django/commit/3ef4bbf495cc6c061789132e3d50a8231a89406b @@ -166,7 +175,7 @@ OK, this is the fun part, where we actually push out a release! classifier in ``setup.py`` to reflect this. Otherwise, make sure the classifier is set to ``Development Status :: 5 - Production/Stable``. -#. Tag the release by running ``git tag`` *FIXME actual commands*. +#. Tag the release by running ``git tag -s`` *FIXME actual commands*. #. ``git push`` your work. @@ -207,7 +216,7 @@ Now you're ready to actually put the release out there. To do this: ``/home/www/djangoproject.com/src/media/pgp``. #. Test that the release packages install correctly using ``easy_install`` - and ``pip``. Here's how I do it (which requires `virtualenvwrapper`__): + and ``pip``. Here's one method (which requires `virtualenvwrapper`__):: $ mktmpenv $ easy_install https://www.djangoproject.com/download//tarball/ @@ -217,20 +226,24 @@ Now you're ready to actually put the release out there. To do this: $ deactivate This just tests that the tarballs are available (i.e. redirects are up) and - that they install correctly, but it'll catch silly mistakes. *XXX FIXME: + that they install correctly, but it'll catch silly mistakes. *FIXME: buildout too?* __ https://pypi.python.org/pypi/virtualenvwrapper -#. Ask a few people on IRC to verify the checksums by visiting the chucksums +#. Ask a few people on IRC to verify the checksums by visiting the checksums file (e.g. https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt) - and following the instructions in it. - -#. If this is a security or regular release, register the new package with - PyPI by uploading the ``PGK-INFO`` file generated in the release package. - This file's *in* the distribution tarball, so you'll need to pull it - out. ``tar xzf dist/Django-.tar.gz Django-/PKG-INFO`` - ought to work. + and following the instructions in it. For bonus points, they can also unpack + the downloaded release tarball and verify that its contents appear to be + correct (proper version numbers, no stray ``.pyc`` or other undesirable + files). + +#. If this is a security or regular release, register the new package with PyPI + by uploading the ``PGK-INFO`` file generated in the release package. This + file's *in* the distribution tarball, so you'll need to pull it out. ``tar + xzf dist/Django-.tar.gz Django-/PKG-INFO`` ought to + work. *FIXME: Is there any reason to pull this file out manually rather than + using "python setup.py register"?* #. Deploy the template changes you made a while back by running `fab deploy` from the ``djangoproject.com`` repo. @@ -240,7 +253,7 @@ Now you're ready to actually put the release out there. To do this: to that page listing the preview package; otherwise, just update the "Get the latest official version" section. -#. Make up the blog post announcing the release live. +#. Make the blog post announcing the release live. #. Post the release announcement to the django-announce, django-developers and django-users mailing lists. This should -- cgit v1.3 From 906dc8522a1745e0e12c8061e4170540f7d0f486 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 25 Feb 2013 10:14:42 -0700 Subject: Fixed #19854 -- Turn Django's own Selenium tests off by default. --- django/contrib/admin/tests.py | 4 ++-- docs/internals/contributing/writing-code/unit-tests.txt | 15 +++------------ tests/regressiontests/views/tests/i18n.py | 4 ++-- tests/runtests.py | 9 ++++----- 4 files changed, 11 insertions(+), 21 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/admin/tests.py b/django/contrib/admin/tests.py index 30d63e4486..badf45b580 100644 --- a/django/contrib/admin/tests.py +++ b/django/contrib/admin/tests.py @@ -10,8 +10,8 @@ class AdminSeleniumWebDriverTestCase(LiveServerTestCase): @classmethod def setUpClass(cls): - if os.environ.get('DJANGO_SKIP_SELENIUM_TESTS', False): - raise SkipTest('Selenium tests skipped by explicit request') + if not os.environ.get('DJANGO_SELENIUM_TESTS', False): + raise SkipTest('Selenium tests not requested') try: cls.selenium = import_by_path(cls.webdriver_class)() except Exception as e: diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index 59f4c97c92..bd1aa5a96f 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -128,21 +128,12 @@ Running the Selenium tests Some admin tests require Selenium 2, Firefox and Python >= 2.6 to work via a real Web browser. To allow those tests to run and not be skipped, you must -install the selenium_ package (version > 2.13) into your Python path. - -Then, run the tests normally, for example: - -.. code-block:: bash - - ./runtests.py --settings=test_sqlite admin_inlines - -If you have Selenium installed but for some reason don't want to run these tests -(for example to speed up the test suite), use the ``--skip-selenium`` option -of the test runner. +install the selenium_ package (version > 2.13) into your Python path and run +the tests with the ``--selenium`` option: .. code-block:: bash - ./runtests.py --settings=test_sqlite --skip-selenium admin_inlines + ./runtests.py --settings=test_sqlite --selenium admin_inlines .. _running-unit-tests-dependencies: diff --git a/tests/regressiontests/views/tests/i18n.py b/tests/regressiontests/views/tests/i18n.py index 206cf3d256..33ffab59f2 100644 --- a/tests/regressiontests/views/tests/i18n.py +++ b/tests/regressiontests/views/tests/i18n.py @@ -177,10 +177,10 @@ class JsI18NTestsMultiPackage(TestCase): javascript_quote('este texto de app3 debe ser traducido')) -skip_selenium = os.environ.get('DJANGO_SKIP_SELENIUM_TESTS', False) +skip_selenium = not os.environ.get('DJANGO_SELENIUM_TESTS', False) -@unittest.skipIf(skip_selenium, 'Selenium tests skipped by explicit request') +@unittest.skipIf(skip_selenium, 'Selenium tests not requested') @unittest.skipUnless(firefox, 'Selenium not installed') class JavascriptI18nTests(LiveServerTestCase): urls = 'regressiontests.views.urls' diff --git a/tests/runtests.py b/tests/runtests.py index f2051a8ea6..800a3f0b93 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -302,10 +302,9 @@ if __name__ == "__main__": 'LiveServerTestCase) is expected to run from. The default value ' 'is localhost:8081.') parser.add_option( - '--skip-selenium', action='store_true', dest='skip_selenium', + '--selenium', action='store_true', dest='selenium', default=False, - help='Skip running Selenium tests even it Selenium itself is ' - 'installed. By default these tests are not skipped.') + help='Run the Selenium tests as well (if Selenium is installed)') options, args = parser.parse_args() if options.settings: os.environ['DJANGO_SETTINGS_MODULE'] = options.settings @@ -318,8 +317,8 @@ if __name__ == "__main__": if options.liveserver is not None: os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options.liveserver - if options.skip_selenium: - os.environ['DJANGO_SKIP_SELENIUM_TESTS'] = '1' + if options.selenium: + os.environ['DJANGO_SELENIUM_TESTS'] = '1' if options.bisect: bisect_tests(options.bisect, options, args) -- cgit v1.3 From 5d883589a8f6950e98538c9509a201d61573460d Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 24 Feb 2013 11:38:34 +0100 Subject: Updated the release process docs to reflect the current practices. Fixed #17919. --- docs/internals/release-process.txt | 156 +++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 74 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/release-process.txt b/docs/internals/release-process.txt index 8affddb5e0..29ce3914b4 100644 --- a/docs/internals/release-process.txt +++ b/docs/internals/release-process.txt @@ -13,12 +13,12 @@ Since version 1.0, Django's release numbering works as follows: * ``A`` is the *major version* number, which is only incremented for major changes to Django, and these changes are not necessarily - backwards-compatible. That is, code you wrote for Django 1.2 may break + backwards-compatible. That is, code you wrote for Django 1.6 may break when we release Django 2.0. * ``B`` is the *minor version* number, which is incremented for large yet - backwards compatible changes. Code written for Django 1.2 will continue - to work under Django 1.3. Exceptions to this rule will be listed in the + backwards compatible changes. Code written for Django 1.6 will continue + to work under Django 1.7. Exceptions to this rule will be listed in the release notes. * ``C`` is the *micro version* number, which is incremented for bug and @@ -27,67 +27,62 @@ Since version 1.0, Django's release numbering works as follows: can't be fixed without breaking backwards-compatibility. If this happens, the release notes will provide detailed upgrade instructions. -* In some cases, we'll make alpha, beta, or release candidate releases. - These are of the form ``A.B alpha/beta/rc N``, which means the ``Nth`` - alpha/beta/release candidate of version ``A.B``. +* Before a new minor release, we'll make alpha, beta, and release candidate + releases. These are of the form ``A.B alpha/beta/rc N``, which means the + ``Nth`` alpha/beta/release candidate of version ``A.B``. -In git, each Django release will have a tag indicating its version -number, signed with the Django release key. Additionally, each release -series (X.Y) has its own branch, and bugfix/security releases will be +In git, each Django release will have a tag indicating its version number, +signed with the Django release key. Additionally, each release series has its +own branch, called ``stable/A.B.x``, and bugfix/security releases will be issued from those branches. -For more information about how the Django project issues new releases -for security purposes, please see :doc:`our security policies -`. +For more information about how the Django project issues new releases for +security purposes, please see :doc:`our security policies `. Major releases -------------- Major releases (1.0, 2.0, etc.) will happen very infrequently (think "years", -not "months"), and will probably represent major, sweeping changes to Django. +not "months"), and may represent major, sweeping changes to Django. Minor releases -------------- -Minor release (1.1, 1.2, etc.) will happen roughly every nine months -- see -`release process`_, below for details. +Minor release (1.5, 1.6, etc.) will happen roughly every nine months -- see +`release process`_, below for details. These releases will contain new +features, improvements to existing features, and such. .. _internal-release-deprecation-policy: -These releases will contain new features, improvements to existing features, and -such. A minor release may deprecate certain features from previous releases. If a -feature in version ``A.B`` is deprecated, it will continue to work in version -``A.B+1``. In version ``A.B+2``, use of the feature will raise a -``DeprecationWarning`` but will continue to work. Version ``A.B+3`` will -remove the feature entirely. +A minor release may deprecate certain features from previous releases. If a +feature is deprecated in version ``A.B``, it will continue to work in versions +``A.B`` and ``A.B+1`` but raise warnings. It will be removed in version +``A.B+2``. -So, for example, if we decided to remove a function that existed in Django 1.0: +So, for example, if we decided to start the deprecation of a function in +Django 1.5: -* Django 1.1 will contain a backwards-compatible replica of the function - which will raise a ``PendingDeprecationWarning``. This warning is silent - by default; you need to explicitly turn on display of these warnings. +* Django 1.5 will contain a backwards-compatible replica of the function which + will raise a ``PendingDeprecationWarning``. This warning is silent by + default; you can turn on display of these warnings with the ``-Wd`` option + of Python. -* Django 1.2 will contain the backwards-compatible replica, but the warning +* Django 1.6 will contain the backwards-compatible replica, but the warning will be promoted to a full-fledged ``DeprecationWarning``. This warning is *loud* by default, and will likely be quite annoying. -* Django 1.3 will remove the feature outright. +* Django 1.7 will remove the feature outright. Micro releases -------------- -Micro releases (1.0.1, 1.0.2, 1.1.1, etc.) will be issued at least once half-way -between minor releases, and probably more often as needed. +Micro releases (1.5.1, 1.6.2, 1.6.1, etc.) will be issued as needed, often to +fix security issues. These releases will be 100% compatible with the associated minor release, unless this is impossible for security reasons. So the answer to "should I upgrade to the latest micro release?" will always be "yes." -Each minor release of Django will have a "release maintainer" appointed. This -person will be responsible for making sure that bug fixes are applied to both -trunk and the maintained micro-release branch. This person will also work with -the release manager to decide when to release the micro releases. - .. _backwards-compatibility-policy: Supported versions @@ -96,10 +91,10 @@ Supported versions At any moment in time, Django's developer team will support a set of releases to varying levels: -* The current development trunk will get new features and bug fixes +* The current development master will get new features and bug fixes requiring major refactoring. -* Patches applied to the trunk will also be applied to the last minor +* Patches applied to the master branch must also be applied to the last minor release, to be released as the next micro release, when they fix critical problems: @@ -111,40 +106,42 @@ varying levels: * Major functionality bugs in newly-introduced features. - The rule of thumb is that fixes will be backported to the last minor - release for bugs that would have prevented a release in the first place. + The rule of thumb is that fixes will be backported to the last minor release + for bugs that would have prevented a release in the first place (release + blockers). -* Security fixes will be applied to the current trunk and the previous two +* Security fixes will be applied to the current master and the previous two minor releases. +* Committers may choose to backport bugfixes at their own discretion, + provided they do not introduce backwards incompatibilities. + * Documentation fixes generally will be more freely backported to the last - release branch, at the discretion of the committer, and they don't need to - meet the "critical fixes only" bar. That's because it's highly advantageous - to have the docs for the last release be up-to-date and correct, and the - downside of backporting (risk of introducing regressions) is much less of a - concern. + release branch. That's because it's highly advantageous to have the docs for + the last release be up-to-date and correct, and the risk of introducing + regressions is much less of a concern. As a concrete example, consider a moment in time halfway between the release of -Django 1.3 and 1.4. At this point in time: +Django 1.6 and 1.7. At this point in time: -* Features will be added to development trunk, to be released as Django 1.4. +* Features will be added to development master, to be released as Django 1.7. -* Critical bug fixes will be applied to a ``1.3.X`` branch, and released as - 1.3.1, 1.3.2, etc. +* Critical bug fixes will be applied to the ``stable/1.6.X`` branch, and + released as 1.6.1, 1.6.2, etc. -* Security fixes will be applied to trunk, a ``1.3.X`` branch and a - ``1.2.X`` branch. They will trigger the release of ``1.3.1``, ``1.2.1``, - etc. +* Security fixes will be applied to ``master``, to the ``stable/1.6.X`` + branch, and to the ``stable/1.5.X`` branch. They will trigger the release of + ``1.6.1``, ``1.5.1``, etc. -* Documentation fixes will be applied to trunk, and, if easily backported, to - the ``1.3.X`` branch. +* Documentation fixes will be applied to master, and, if easily backported, to + the ``1.6.X`` branch. Bugfixes may also be backported. .. _release-process: Release process =============== -Django uses a time-based release schedule, with minor (i.e. 1.1, 1.2, etc.) +Django uses a time-based release schedule, with minor (i.e. 1.6, 1.7, etc.) releases every nine months, or more, depending on features. After each release, and after a suitable cooling-off period of a few weeks, the @@ -190,45 +187,56 @@ At the end of phase two, any unfinished "maybe" features will be postponed until the next release. Though it shouldn't happen, any "must-have" features will extend phase two, and thus postpone the final release. -Phase two will culminate with an alpha release. +Phase two will culminate with an alpha release. At this point, the +``stable/A.B.x`` branch will be forked from ``master``. Phase three: bugfixes ~~~~~~~~~~~~~~~~~~~~~ The last third of a release is spent fixing bugs -- no new features will be -accepted during this time. We'll release a beta release about halfway through, -and an rc complete with string freeze two weeks before the end of the schedule. +accepted during this time. We'll try to release a beta release after one month +and a release candidate after two months. + +The release candidate marks the string freeze, and it happens at least two +weeks before the final release. After this point, new translatable strings +must not be added. + +During this phase, committers will be more and more conservative with +backports, to avoid introducing regressions. After the release candidate, only +release blockers and documentation fixes should be backported. + +In parallel to this phase, ``master`` can receive new features, to be released +in the ``A.B+1`` cycle. Bug-fix releases ---------------- -After a minor release (e.g. 1.1), the previous release will go into bugfix +After a minor release (e.g. 1.6), the previous release will go into bugfix mode. -A branch will be created of the form ``branches/releases/1.0.X`` to track -bugfixes to the previous release. Critical bugs fixed on trunk must -*also* be fixed on the bugfix branch; this means that commits need to cleanly -separate bug fixes from feature additions. The developer who commits a fix to -trunk will be responsible for also applying the fix to the current bugfix -branch. Each bugfix branch will have a maintainer who will work with the -committers to keep them honest on backporting bug fixes. +A branch will be created of the form ``stable/1.5.x`` to track bugfixes to the +previous release. Critical bugs fixed on master must *also* be fixed on the +bugfix branch; this means that commits need to cleanly separate bug fixes from +feature additions. The developer who commits a fix to master will be +responsible for also applying the fix to the current bugfix branch. How this all fits together -------------------------- Let's look at a hypothetical example for how this all first together. Imagine, -if you will, a point about halfway between 1.1 and 1.2. At this point, +if you will, a point about halfway between 1.5 and 1.6. At this point, development will be happening in a bunch of places: -* On trunk, development towards 1.2 proceeds with small additions, bugs +* On master, development towards 1.6 proceeds with small additions, bugs fixes, etc. being checked in daily. -* On the branch "branches/releases/1.1.X", fixes for critical bugs found in - the 1.1 release are checked in as needed. At some point, this branch will - be released as "1.1.1", "1.1.2", etc. +* On the branch ``stable/1.5.x``, fixes for critical bugs found in + the 1.5 release are checked in as needed. At some point, this branch will + be released as "1.5.1", "1.5.2", etc. -* On the branch "branches/releases/1.0.X", security fixes are made if - needed and released as "1.0.2", "1.0.3", etc. +* On the branch ``stable/1.4.x``, security fixes are made if + needed and released as "1.4.2", "1.4.3", etc. -* On feature branches, development of major features is done. These - branches will be merged into trunk before the end of phase two. +* Development of major features is done in branches in forks of the main + repository. These branches will be merged into ``master`` before "1.6 + alpha 1". -- cgit v1.3 From 28e545c4b3c18fd1d3641b1194bf53699a7c868a Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Tue, 26 Feb 2013 15:00:16 +0100 Subject: Updated docs to reflect new tests layout. Thanks to Ramiro Morales for the initial patch. --- docs/internals/contributing/writing-code/unit-tests.txt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/writing-code/unit-tests.txt b/docs/internals/contributing/writing-code/unit-tests.txt index bd1aa5a96f..f56bf1cdeb 100644 --- a/docs/internals/contributing/writing-code/unit-tests.txt +++ b/docs/internals/contributing/writing-code/unit-tests.txt @@ -7,10 +7,8 @@ code base. It's our policy to make sure all tests pass at all times. The tests cover: -* Models and the database API (``tests/modeltests``), -* Everything else in core Django code (``tests/regressiontests``), -* :ref:`contrib-apps` (``django/contrib//tests`` or - ``tests/regressiontests/_...``). +* Models, the database API and everything else in core Django core (``tests/``), +* :ref:`contrib-apps` (``django/contrib//tests`` or ``tests/_...``). We appreciate any and all contributions to the test suite! @@ -105,9 +103,9 @@ internationalization, type: ./runtests.py --settings=path.to.settings generic_relations i18n -How do you find out the names of individual tests? Look in -``tests/modeltests`` and ``tests/regressiontests`` — each directory name -there is the name of a test. Contrib app names are also valid test names. +How do you find out the names of individual tests? Look in ``tests/`` — each +directory name there is the name of a test. Contrib app names are also valid +test names. If you just want to run a particular class of tests, you can specify a list of paths to individual test classes. For example, to run the ``TranslationTests`` -- cgit v1.3 From 9e6725c5eabfaf0f0153b4d40390b4916fd7d9eb Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 26 Feb 2013 13:46:32 -0700 Subject: Added note about updating default docs version in howto-release doc. --- docs/internals/howto-release-django.txt | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index fffdc9b869..118d09b575 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -260,6 +260,13 @@ Now you're ready to actually put the release out there. To do this: include links to both the announcement blog post and the release notes. *FIXME: make some templates with example text*. +#. For a new version release (e.g. 1.5, 1.6), update the default stable version + of the docs by flipping the ``is_default`` flag to ``True`` on the + appropriate ``DocumentRelease`` object in the ``docs.djangoproject.com`` + database (this will automatically flip it to ``False`` for all + others). *FIXME: I had to do this via fab managepy:shell,docs but we should + probably make it possible to do via the admin.* + Post-release ============ -- cgit v1.3 From bd669e47d7331ee41f929486e48ed39ad7bec1a7 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 26 Feb 2013 14:46:46 -0700 Subject: Added a note about creating new doc versions; update stable doc version before announcement. --- docs/internals/howto-release-django.txt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 118d09b575..9e8b64e356 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -255,11 +255,6 @@ Now you're ready to actually put the release out there. To do this: #. Make the blog post announcing the release live. -#. Post the release announcement to the django-announce, - django-developers and django-users mailing lists. This should - include links to both the announcement blog post and the release - notes. *FIXME: make some templates with example text*. - #. For a new version release (e.g. 1.5, 1.6), update the default stable version of the docs by flipping the ``is_default`` flag to ``True`` on the appropriate ``DocumentRelease`` object in the ``docs.djangoproject.com`` @@ -267,6 +262,11 @@ Now you're ready to actually put the release out there. To do this: others). *FIXME: I had to do this via fab managepy:shell,docs but we should probably make it possible to do via the admin.* +#. Post the release announcement to the django-announce, + django-developers and django-users mailing lists. This should + include links to both the announcement blog post and the release + notes. *FIXME: make some templates with example text*. + Post-release ============ @@ -277,6 +277,12 @@ You're almost done! All that's left to do now is: example, after releasing 1.2.1, update ``VERSION`` to report "1.2.2 pre-alpha". *FIXME: Is this correct? Do we still do this?* +#. For the first alpha release of a new version (when we create the + ``stable/1.?.x`` git branch), you'll want to create a new + ``DocumentRelease`` object in the ``docs.djangoproject.com`` database for + the new version's docs, and update the ``docs/fixtures/doc_releases.json`` + JSON fixture. *FIXME: what is the purpose of maintaining this fixture?* + Notes on setting the VERSION tuple ================================== -- cgit v1.3 From ab1c25f674f8fdaa06897ce5ce65e559c0a6de1b Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 28 Feb 2013 10:26:47 +0100 Subject: Added a Trac-related item to the release checklist. --- docs/internals/howto-release-django.txt | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 9e8b64e356..83b6a8c9be 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -283,6 +283,10 @@ You're almost done! All that's left to do now is: the new version's docs, and update the ``docs/fixtures/doc_releases.json`` JSON fixture. *FIXME: what is the purpose of maintaining this fixture?* +#. Add the release in `Trac's versions list`_. + +.. _Trac's versions list: https://code.djangoproject.com/admin/ticket/versions + Notes on setting the VERSION tuple ================================== -- cgit v1.3 From 2ee21d9f0d9eaed0494f3b9cd4b5bc9beffffae5 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 18 Feb 2013 11:37:26 +0100 Subject: Implemented persistent database connections. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Anssi Kääriäinen and Karen Tracey for their inputs. --- django/contrib/auth/handlers/modwsgi.py | 4 +- django/db/__init__.py | 21 ++++++--- django/db/backends/__init__.py | 32 ++++++++++++- django/db/backends/mysql/base.py | 8 ++++ django/db/backends/oracle/base.py | 12 +++++ django/db/backends/postgresql_psycopg2/base.py | 9 ++++ django/db/backends/sqlite3/base.py | 3 ++ django/db/utils.py | 15 +++++-- django/test/client.py | 12 ++--- docs/internals/deprecation.txt | 2 + docs/ref/databases.txt | 62 ++++++++++++++++++++++++++ docs/ref/settings.txt | 13 ++++++ docs/releases/1.6.txt | 21 +++++++++ tests/handlers/tests.py | 17 ++++--- tests/httpwrappers/tests.py | 6 +-- tests/wsgi/tests.py | 8 ++++ 16 files changed, 220 insertions(+), 25 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/auth/handlers/modwsgi.py b/django/contrib/auth/handlers/modwsgi.py index df20f1283a..f14afcf290 100644 --- a/django/contrib/auth/handlers/modwsgi.py +++ b/django/contrib/auth/handlers/modwsgi.py @@ -25,7 +25,7 @@ def check_password(environ, username, password): return None return user.check_password(password) finally: - db.close_connection() + db.close_old_connections() def groups_for_user(environ, username): """ @@ -44,4 +44,4 @@ def groups_for_user(environ, username): return [] return [force_bytes(group.name) for group in user.groups.all()] finally: - db.close_connection() + db.close_old_connections() diff --git a/django/db/__init__.py b/django/db/__init__.py index e76c6c3268..5e630392e7 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -42,9 +42,10 @@ class DefaultConnectionProxy(object): connection = DefaultConnectionProxy() backend = load_backend(connection.settings_dict['ENGINE']) -# Register an event that closes the database connection -# when a Django request is finished. def close_connection(**kwargs): + warnings.warn( + "close_connection is superseded by close_old_connections.", + PendingDeprecationWarning, stacklevel=2) # Avoid circular imports from django.db import transaction for conn in connections: @@ -53,15 +54,25 @@ def close_connection(**kwargs): # connection state will be cleaned up. transaction.abort(conn) connections[conn].close() -signals.request_finished.connect(close_connection) -# Register an event that resets connection.queries -# when a Django request is started. +# Register an event to reset saved queries when a Django request is started. def reset_queries(**kwargs): for conn in connections.all(): conn.queries = [] signals.request_started.connect(reset_queries) +# Register an event to reset transaction state and close connections past +# their lifetime. NB: abort() doesn't do anything outside of a transaction. +def close_old_connections(**kwargs): + for conn in connections.all(): + try: + conn.abort() + except DatabaseError: + pass + conn.close_if_unusable_or_obsolete() +signals.request_started.connect(close_old_connections) +signals.request_finished.connect(close_old_connections) + # Register an event that rolls back the connections # when a Django request has an exception. def _rollback_on_exception(**kwargs): diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 49e07cfa9e..9fb2b23644 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1,4 +1,5 @@ import datetime +import time from django.db.utils import DatabaseError @@ -49,6 +50,10 @@ class BaseDatabaseWrapper(object): self._thread_ident = thread.get_ident() self.allow_thread_sharing = allow_thread_sharing + # Connection termination related attributes + self.close_at = None + self.errors_occurred = False + def __eq__(self, other): return self.alias == other.alias @@ -59,7 +64,7 @@ class BaseDatabaseWrapper(object): return hash(self.alias) def wrap_database_errors(self): - return DatabaseErrorWrapper(self.Database) + return DatabaseErrorWrapper(self) def get_connection_params(self): raise NotImplementedError @@ -76,6 +81,11 @@ class BaseDatabaseWrapper(object): def _cursor(self): with self.wrap_database_errors(): if self.connection is None: + # Reset parameters defining when to close the connection + max_age = self.settings_dict['CONN_MAX_AGE'] + self.close_at = None if max_age is None else time.time() + max_age + self.errors_occurred = False + # Establish the connection conn_params = self.get_connection_params() self.connection = self.get_new_connection(conn_params) self.init_connection_state() @@ -351,6 +361,26 @@ class BaseDatabaseWrapper(object): self.connection = None self.set_clean() + def close_if_unusable_or_obsolete(self): + if self.connection is not None: + if self.errors_occurred: + if self.is_usable(): + self.errors_occurred = False + else: + self.close() + return + if self.close_at is not None and time.time() >= self.close_at: + self.close() + return + + def is_usable(self): + """ + Test if the database connection is usable. + + This function may assume that self.connection is not None. + """ + raise NotImplementedError + def cursor(self): self.validate_thread_sharing() if (self.use_debug_cursor or diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 4bfd3c4481..6b2ecaead1 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -439,6 +439,14 @@ class DatabaseWrapper(BaseDatabaseWrapper): cursor = self.connection.cursor() return CursorWrapper(cursor) + def is_usable(self): + try: + self.connection.ping() + except DatabaseError: + return False + else: + return True + def _rollback(self): try: BaseDatabaseWrapper._rollback(self) diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index d35e814d1f..478124f5df 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -598,6 +598,18 @@ class DatabaseWrapper(BaseDatabaseWrapper): # stmtcachesize is available only in 4.3.2 and up. pass + def is_usable(self): + try: + if hasattr(self.connection, 'ping'): # Oracle 10g R2 and higher + self.connection.ping() + else: + # Use a cx_Oracle cursor directly, bypassing Django's utilities. + self.connection.cursor().execute("SELECT 1 FROM DUAL") + except DatabaseError: + return False + else: + return True + # Oracle doesn't support savepoint commits. Ignore them. def _savepoint_commit(self, sid): pass diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index fb04072494..db4b5ade05 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -177,6 +177,15 @@ class DatabaseWrapper(BaseDatabaseWrapper): cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None return cursor + def is_usable(self): + try: + # Use a psycopg cursor directly, bypassing Django's utilities. + self.connection.cursor().execute("SELECT 1") + except DatabaseError: + return False + else: + return True + def _enter_transaction_management(self, managed): """ Switch the isolation level when needing transaction support, so that diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 6bf1ffc469..ad54af46ad 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -347,6 +347,9 @@ class DatabaseWrapper(BaseDatabaseWrapper): def create_cursor(self): return self.connection.cursor(factory=SQLiteCursorWrapper) + def is_usable(self): + return True + def check_constraints(self, table_names=None): """ Checks each table name in `table_names` for rows with invalid foreign key references. This method is diff --git a/django/db/utils.py b/django/db/utils.py index 0c98cc23fd..cc17b3e7a3 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -56,11 +56,13 @@ class DatabaseErrorWrapper(object): exceptions using Django's common wrappers. """ - def __init__(self, database): + def __init__(self, wrapper): """ - database is a module defining PEP-249 exceptions. + wrapper is a database wrapper. + + It must have a Database attribute defining PEP-249 exceptions. """ - self.database = database + self.wrapper = wrapper def __enter__(self): pass @@ -79,7 +81,7 @@ class DatabaseErrorWrapper(object): InterfaceError, Error, ): - db_exc_type = getattr(self.database, dj_exc_type.__name__) + db_exc_type = getattr(self.wrapper.Database, dj_exc_type.__name__) if issubclass(exc_type, db_exc_type): # Under Python 2.6, exc_value can still be a string. try: @@ -89,6 +91,10 @@ class DatabaseErrorWrapper(object): dj_exc_value = dj_exc_type(*args) if six.PY3: dj_exc_value.__cause__ = exc_value + # Only set the 'errors_occurred' flag for errors that may make + # the connection unusable. + if dj_exc_type not in (DataError, IntegrityError): + self.wrapper.errors_occurred = True six.reraise(dj_exc_type, dj_exc_value, traceback) def __call__(self, func): @@ -155,6 +161,7 @@ class ConnectionHandler(object): conn.setdefault('ENGINE', 'django.db.backends.dummy') if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']: conn['ENGINE'] = 'django.db.backends.dummy' + conn.setdefault('CONN_MAX_AGE', 600) conn.setdefault('OPTIONS', {}) conn.setdefault('TIME_ZONE', 'UTC' if settings.USE_TZ else settings.TIME_ZONE) for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']: diff --git a/django/test/client.py b/django/test/client.py index 2506437023..46f55d7cdc 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -18,7 +18,7 @@ from django.core.handlers.base import BaseHandler from django.core.handlers.wsgi import WSGIRequest from django.core.signals import (request_started, request_finished, got_request_exception) -from django.db import close_connection +from django.db import close_old_connections from django.http import SimpleCookie, HttpRequest, QueryDict from django.template import TemplateDoesNotExist from django.test import signals @@ -78,9 +78,9 @@ def closing_iterator_wrapper(iterable, close): for item in iterable: yield item finally: - request_finished.disconnect(close_connection) + request_finished.disconnect(close_old_connections) close() # will fire request_finished - request_finished.connect(close_connection) + request_finished.connect(close_old_connections) class ClientHandler(BaseHandler): @@ -101,7 +101,9 @@ class ClientHandler(BaseHandler): if self._request_middleware is None: self.load_middleware() + request_started.disconnect(close_old_connections) request_started.send(sender=self.__class__) + request_started.connect(close_old_connections) request = WSGIRequest(environ) # sneaky little hack so that we can easily get round # CsrfViewMiddleware. This makes life easier, and is probably @@ -115,9 +117,9 @@ class ClientHandler(BaseHandler): response.streaming_content = closing_iterator_wrapper( response.streaming_content, response.close) else: - request_finished.disconnect(close_connection) + request_finished.disconnect(close_old_connections) response.close() # will fire request_finished - request_finished.connect(close_connection) + request_finished.connect(close_old_connections) return response diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index f1ae1338df..3a9cbd195d 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -339,6 +339,8 @@ these changes. * ``Model._meta.module_name`` was renamed to ``model_name``. +* The private API ``django.db.close_connection`` will be removed. + 2.0 --- diff --git a/docs/ref/databases.txt b/docs/ref/databases.txt index e933ee350d..34f60e99ac 100644 --- a/docs/ref/databases.txt +++ b/docs/ref/databases.txt @@ -11,6 +11,68 @@ This file describes some of the features that might be relevant to Django usage. Of course, it is not intended as a replacement for server-specific documentation or reference manuals. +General notes +============= + +.. _persistent-database-connections: + +Persistent connections +---------------------- + +.. versionadded:: 1.6 + +Persistent connections avoid the overhead of re-establishing a connection to +the database in each request. By default, connections are kept open for up 10 +minutes — if not specified, :setting:`CONN_MAX_AGE` defaults to 600 seconds. + +Django 1.5 and earlier didn't have persistent connections. To restore the +legacy behavior of closing the connection at the end of every request, set +:setting:`CONN_MAX_AGE` to ``0``. + +For unlimited persistent connections, set :setting:`CONN_MAX_AGE` to ``None``. + +Connection management +~~~~~~~~~~~~~~~~~~~~~ + +Django opens a connection to the database when it first makes a database +query. It keeps this connection open and reuses it in subsequent requests. +Django closes the connection once it exceeds the maximum age defined by +:setting:`CONN_MAX_AGE` or when it isn't usable any longer. + +In detail, Django automatically opens a connection to the database whenever it +needs one and doesn't have one already — either because this is the first +connection, or because the previous connection was closed. + +At the beginning of each request, Django closes the connection if it has +reached its maximum age. If your database terminates idle connections after +some time, you should set :setting:`CONN_MAX_AGE` to a lower value, so that +Django doesn't attempt to use a connection that has been terminated by the +database server. (This problem may only affect very low traffic sites.) + +At the end of each request, Django closes the connection if it has reached its +maximum age or if it is in an unrecoverable error state. If any database +errors have occurred while processing the requests, Django checks whether the +connection still works, and closes it if it doesn't. Thus, database errors +affect at most one request; if the connection becomes unusable, the next +request gets a fresh connection. + +Caveats +~~~~~~~ + +Since each thread maintains its own connection, your database must support at +least as many simultaneous connections as you have worker threads. + +Sometimes a database won't be accessed by the majority of your views, for +example because it's the database of an external system, or thanks to caching. +In such cases, you should set :setting:`CONN_MAX_AGE` to a lower value, or +even ``0``, because it doesn't make sense to maintain a connection that's +unlikely to be reused. This will help keep the number of simultaneous +connections to this database small. + + +The development server creates a new thread for each request it handles, +negating the effect of persistent connections. + .. _postgresql-notes: PostgreSQL notes diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index bba936d837..baeb02c32d 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -464,6 +464,19 @@ The name of the database to use. For SQLite, it's the full path to the database file. When specifying the path, always use forward slashes, even on Windows (e.g. ``C:/homes/user/mysite/sqlite3.db``). +.. setting:: CONN_MAX_AGE + +CONN_MAX_AGE +~~~~~~~~~~~~ + +.. versionadded:: 1.6 + +Default: ``600`` + +The lifetime of a database connection, in seconds. Use ``0`` to close database +connections at the end of each request — Django's historical behavior — and +``None`` for unlimited persistent connections. + .. setting:: OPTIONS OPTIONS diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index c6a4fb2d5d..34fa687290 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -30,6 +30,19 @@ prevention ` are turned on. If the default templates don't suit your tastes, you can use :ref:`custom project and app templates `. +Persistent database connections +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django now supports reusing the same database connection for several requests. +This avoids the overhead of re-establishing a connection at the beginning of +each request. + +By default, database connections will kept open for 10 minutes. This behavior +is controlled by the :setting:`CONN_MAX_AGE` setting. To restore the previous +behavior of closing the connection at the end of each request, set +:setting:`CONN_MAX_AGE` to ``0``. See :ref:`persistent-database-connections` +for details. + Time zone aware aggregation ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -136,6 +149,14 @@ Backwards incompatible changes in 1.6 * Model fields named ``hour``, ``minute`` or ``second`` may clash with the new lookups. Append an explicit :lookup:`exact` lookup if this is an issue. +* When Django establishes a connection to the database, it sets up appropriate + parameters, depending on the backend being used. Since `persistent database + connections `_ are enabled by default in + Django 1.6, this setup isn't repeated at every request any more. If you + modifiy parameters such as the connection's isolation level or time zone, + you should either restore Django's defaults at the end of each request, or + force an appropriate value at the beginning of each request. + * If your CSS/Javascript code used to access HTML input widgets by type, you should review it as ``type='text'`` widgets might be now output as ``type='email'``, ``type='url'`` or ``type='number'`` depending on their diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index f647bf199b..6eb9bd23fe 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -1,5 +1,6 @@ from django.core.handlers.wsgi import WSGIHandler -from django.core import signals +from django.core.signals import request_started, request_finished +from django.db import close_old_connections from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.utils import six @@ -7,6 +8,12 @@ from django.utils import six class HandlerTests(TestCase): + def setUp(self): + request_started.disconnect(close_old_connections) + + def tearDown(self): + request_started.connect(close_old_connections) + # Mangle settings so the handler will fail @override_settings(MIDDLEWARE_CLASSES=42) def test_lock_safety(self): @@ -35,12 +42,12 @@ class SignalsTests(TestCase): def setUp(self): self.signals = [] - signals.request_started.connect(self.register_started) - signals.request_finished.connect(self.register_finished) + request_started.connect(self.register_started) + request_finished.connect(self.register_finished) def tearDown(self): - signals.request_started.disconnect(self.register_started) - signals.request_finished.disconnect(self.register_finished) + request_started.disconnect(self.register_started) + request_finished.disconnect(self.register_finished) def register_started(self, **kwargs): self.signals.append('started') diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 2964a86034..194232e92f 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -8,7 +8,7 @@ import warnings from django.core.exceptions import SuspiciousOperation from django.core.signals import request_finished -from django.db import close_connection +from django.db import close_old_connections from django.http import (QueryDict, HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, HttpResponseNotAllowed, HttpResponseNotModified, StreamingHttpResponse, @@ -490,10 +490,10 @@ class FileCloseTests(TestCase): def setUp(self): # Disable the request_finished signal during this test # to avoid interfering with the database connection. - request_finished.disconnect(close_connection) + request_finished.disconnect(close_old_connections) def tearDown(self): - request_finished.connect(close_connection) + request_finished.connect(close_old_connections) def test_response(self): filename = os.path.join(os.path.dirname(upath(__file__)), 'abc.txt') diff --git a/tests/wsgi/tests.py b/tests/wsgi/tests.py index 9b7ee68afd..a66258d4eb 100644 --- a/tests/wsgi/tests.py +++ b/tests/wsgi/tests.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.core.servers.basehttp import get_internal_wsgi_application +from django.core.signals import request_started from django.core.wsgi import get_wsgi_application +from django.db import close_old_connections from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings @@ -12,6 +14,12 @@ from django.utils import six, unittest class WSGITest(TestCase): urls = "wsgi.urls" + def setUp(self): + request_started.disconnect(close_old_connections) + + def tearDown(self): + request_started.connect(close_old_connections) + def test_get_wsgi_application(self): """ Verify that ``get_wsgi_application`` returns a functioning WSGI -- cgit v1.3 From 6983a1a540a6e6c3bd941fa15ddd8cb49f9ec74e Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Fri, 8 Mar 2013 09:15:23 -0500 Subject: Fixed #15363 -- Renamed and normalized to `get_queryset` the methods that return a QuerySet. --- AUTHORS | 2 +- django/contrib/admin/options.py | 31 +++-- django/contrib/admin/templatetags/admin_list.py | 10 +- django/contrib/admin/views/main.py | 39 ++++-- django/contrib/auth/admin.py | 2 +- django/contrib/comments/managers.py | 4 +- django/contrib/comments/templatetags/comments.py | 17 ++- django/contrib/contenttypes/generic.py | 20 ++- django/contrib/gis/db/models/manager.py | 62 ++++----- django/contrib/sites/managers.py | 4 +- django/core/serializers/__init__.py | 2 +- django/db/models/fields/related.py | 46 ++++--- django/db/models/manager.py | 84 ++++++------ django/db/models/query.py | 14 +- django/forms/models.py | 6 +- django/utils/deprecation.py | 62 +++++++++ docs/faq/admin.txt | 2 +- docs/internals/deprecation.txt | 9 ++ docs/ref/contrib/admin/index.txt | 15 ++- docs/ref/models/querysets.txt | 18 +-- docs/releases/1.6.txt | 6 + docs/topics/db/managers.txt | 25 ++-- docs/topics/db/multi-db.txt | 16 +-- tests/admin_changelist/admin.py | 8 +- tests/admin_changelist/models.py | 4 +- tests/admin_changelist/tests.py | 14 +- tests/admin_filters/tests.py | 50 +++---- tests/admin_ordering/tests.py | 20 +-- tests/admin_views/admin.py | 36 +++--- tests/admin_views/customadmin.py | 4 +- tests/admin_views/tests.py | 12 +- tests/admin_widgets/models.py | 4 +- tests/custom_managers/models.py | 10 +- tests/custom_managers_regress/models.py | 4 +- tests/deprecation/__init__.py | 0 tests/deprecation/models.py | 0 tests/deprecation/tests.py | 158 +++++++++++++++++++++++ tests/fixtures/models.py | 4 +- tests/generic_relations/models.py | 4 +- tests/get_object_or_404/models.py | 4 +- tests/managers_regress/models.py | 12 +- tests/modeladmin/tests.py | 4 +- tests/prefetch_related/models.py | 4 +- tests/proxy_models/models.py | 8 +- tests/queries/models.py | 8 +- tests/reverse_single_related/models.py | 4 +- 46 files changed, 588 insertions(+), 284 deletions(-) create mode 100644 django/utils/deprecation.py create mode 100644 tests/deprecation/__init__.py create mode 100644 tests/deprecation/models.py create mode 100644 tests/deprecation/tests.py (limited to 'docs/internals') diff --git a/AUTHORS b/AUTHORS index c6d7bf0414..35a316d4c2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -98,7 +98,7 @@ answer newbie questions, and generally made Django that much better: Natalia Bidart Mark Biggers Paul Bissex - Loic Bistuer + Loïc Bistuer Simon Blanchard Craig Blaszczyk David Blewett diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 567f7cf990..de7614ff24 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -29,6 +29,7 @@ from django.utils.datastructures import SortedDict from django.utils.html import escape, escapejs from django.utils.safestring import mark_safe from django.utils import six +from django.utils.deprecation import RenameMethodsBase from django.utils.text import capfirst, get_text_list from django.utils.translation import ugettext as _ from django.utils.translation import ungettext @@ -64,7 +65,13 @@ FORMFIELD_FOR_DBFIELD_DEFAULTS = { csrf_protect_m = method_decorator(csrf_protect) -class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): +class RenameBaseModelAdminMethods(forms.MediaDefiningClass, RenameMethodsBase): + renamed_methods = ( + ('queryset', 'get_queryset', PendingDeprecationWarning), + ) + + +class BaseModelAdmin(six.with_metaclass(RenameBaseModelAdminMethods)): """Functionality common to both ModelAdmin and InlineAdmin.""" raw_id_fields = () @@ -239,12 +246,12 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)): """ return self.prepopulated_fields - def queryset(self, request): + def get_queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the admin site. This is used by changelist_view. """ - qs = self.model._default_manager.get_query_set() + qs = self.model._default_manager.get_queryset() # TODO: this should be handled by some parameter to the ChangeList. ordering = self.get_ordering(request) if ordering: @@ -496,7 +503,7 @@ class ModelAdmin(BaseModelAdmin): returned if no match is found (or the object_id failed validation against the primary key field). """ - queryset = self.queryset(request) + queryset = self.get_queryset(request) model = queryset.model try: object_id = model._meta.pk.to_python(object_id) @@ -1008,7 +1015,7 @@ class ModelAdmin(BaseModelAdmin): formset = FormSet(data=request.POST, files=request.FILES, instance=new_object, save_as_new="_saveasnew" in request.POST, - prefix=prefix, queryset=inline.queryset(request)) + prefix=prefix, queryset=inline.get_queryset(request)) formsets.append(formset) if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, False) @@ -1034,7 +1041,7 @@ class ModelAdmin(BaseModelAdmin): if prefixes[prefix] != 1 or not prefix: prefix = "%s-%s" % (prefix, prefixes[prefix]) formset = FormSet(instance=self.model(), prefix=prefix, - queryset=inline.queryset(request)) + queryset=inline.get_queryset(request)) formsets.append(formset) adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), @@ -1104,7 +1111,7 @@ class ModelAdmin(BaseModelAdmin): prefix = "%s-%s" % (prefix, prefixes[prefix]) formset = FormSet(request.POST, request.FILES, instance=new_object, prefix=prefix, - queryset=inline.queryset(request)) + queryset=inline.get_queryset(request)) formsets.append(formset) @@ -1124,7 +1131,7 @@ class ModelAdmin(BaseModelAdmin): if prefixes[prefix] != 1 or not prefix: prefix = "%s-%s" % (prefix, prefixes[prefix]) formset = FormSet(instance=obj, prefix=prefix, - queryset=inline.queryset(request)) + queryset=inline.get_queryset(request)) formsets.append(formset) adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), @@ -1209,7 +1216,7 @@ class ModelAdmin(BaseModelAdmin): if (actions and request.method == 'POST' and 'index' in request.POST and '_save' not in request.POST): if selected: - response = self.response_action(request, queryset=cl.get_query_set(request)) + response = self.response_action(request, queryset=cl.get_queryset(request)) if response: return response else: @@ -1225,7 +1232,7 @@ class ModelAdmin(BaseModelAdmin): helpers.ACTION_CHECKBOX_NAME in request.POST and 'index' not in request.POST and '_save' not in request.POST): if selected: - response = self.response_action(request, queryset=cl.get_query_set(request)) + response = self.response_action(request, queryset=cl.get_queryset(request)) if response: return response else: @@ -1521,8 +1528,8 @@ class InlineModelAdmin(BaseModelAdmin): fields = list(form.base_fields) + list(self.get_readonly_fields(request, obj)) return [(None, {'fields': fields})] - def queryset(self, request): - queryset = super(InlineModelAdmin, self).queryset(request) + def get_queryset(self, request): + queryset = super(InlineModelAdmin, self).get_queryset(request) if not self.has_change_permission(request): queryset = queryset.none() return queryset diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index c08193d238..18a45a006f 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -306,8 +306,8 @@ def date_hierarchy(cl): if not (year_lookup or month_lookup or day_lookup): # select appropriate start level - date_range = cl.query_set.aggregate(first=models.Min(field_name), - last=models.Max(field_name)) + date_range = cl.queryset.aggregate(first=models.Min(field_name), + last=models.Max(field_name)) if date_range['first'] and date_range['last']: if date_range['first'].year == date_range['last'].year: year_lookup = date_range['first'].year @@ -325,7 +325,7 @@ def date_hierarchy(cl): 'choices': [{'title': capfirst(formats.date_format(day, 'MONTH_DAY_FORMAT'))}] } elif year_lookup and month_lookup: - days = cl.query_set.filter(**{year_field: year_lookup, month_field: month_lookup}) + days = cl.queryset.filter(**{year_field: year_lookup, month_field: month_lookup}) days = getattr(days, dates_or_datetimes)(field_name, 'day') return { 'show': True, @@ -339,7 +339,7 @@ def date_hierarchy(cl): } for day in days] } elif year_lookup: - months = cl.query_set.filter(**{year_field: year_lookup}) + months = cl.queryset.filter(**{year_field: year_lookup}) months = getattr(months, dates_or_datetimes)(field_name, 'month') return { 'show': True, @@ -353,7 +353,7 @@ def date_hierarchy(cl): } for month in months] } else: - years = getattr(cl.query_set, dates_or_datetimes)(field_name, 'year') + years = getattr(cl.queryset, dates_or_datetimes)(field_name, 'year') return { 'show': True, 'choices': [{ diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 8bda323d83..b7bf85ef9d 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,4 +1,5 @@ import operator +import warnings from functools import reduce from django.core.exceptions import SuspiciousOperation, ImproperlyConfigured @@ -6,7 +7,9 @@ from django.core.paginator import InvalidPage from django.core.urlresolvers import reverse from django.db import models from django.db.models.fields import FieldDoesNotExist +from django.utils import six from django.utils.datastructures import SortedDict +from django.utils.deprecation import RenameMethodsBase from django.utils.encoding import force_str, force_text from django.utils.translation import ugettext, ugettext_lazy from django.utils.http import urlencode @@ -33,14 +36,20 @@ IGNORED_PARAMS = ( EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)') -class ChangeList(object): +class RenameChangeListMethods(RenameMethodsBase): + renamed_methods = ( + ('get_query_set', 'get_queryset', PendingDeprecationWarning), + ) + + +class ChangeList(six.with_metaclass(RenameChangeListMethods)): def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_max_show_all, list_editable, model_admin): self.model = model self.opts = model._meta self.lookup_opts = self.opts - self.root_query_set = model_admin.queryset(request) + self.root_queryset = model_admin.get_queryset(request) self.list_display = list_display self.list_display_links = list_display_links self.list_filter = list_filter @@ -70,7 +79,7 @@ class ChangeList(object): else: self.list_editable = list_editable self.query = request.GET.get(SEARCH_VAR, '') - self.query_set = self.get_query_set(request) + self.queryset = self.get_queryset(request) self.get_results(request) if self.is_popup: title = ugettext('Select %s') @@ -79,6 +88,20 @@ class ChangeList(object): self.title = title % force_text(self.opts.verbose_name) self.pk_attname = self.lookup_opts.pk.attname + @property + def root_query_set(self): + warnings.warn("`ChangeList.root_query_set` is deprecated, " + "use `root_queryset` instead.", + PendingDeprecationWarning, 2) + return self.root_queryset + + @property + def query_set(self): + warnings.warn("`ChangeList.query_set` is deprecated, " + "use `queryset` instead.", + PendingDeprecationWarning, 2) + return self.queryset + def get_filters_params(self, params=None): """ Returns all params except IGNORED_PARAMS @@ -169,7 +192,7 @@ class ChangeList(object): return '?%s' % urlencode(sorted(p.items())) def get_results(self, request): - paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page) + paginator = self.model_admin.get_paginator(request, self.queryset, self.list_per_page) # Get the number of objects, with admin filters applied. result_count = paginator.count @@ -178,7 +201,7 @@ class ChangeList(object): # full_result_count is equal to paginator.count if no filters # were applied if self.get_filters_params(): - full_result_count = self.root_query_set.count() + full_result_count = self.root_queryset.count() else: full_result_count = result_count can_show_all = result_count <= self.list_max_show_all @@ -186,7 +209,7 @@ class ChangeList(object): # Get the list of objects to display on this page. if (self.show_all and can_show_all) or not multi_page: - result_list = self.query_set._clone() + result_list = self.queryset._clone() else: try: result_list = paginator.page(self.page_num+1).object_list @@ -304,13 +327,13 @@ class ChangeList(object): ordering_fields[idx] = 'desc' if pfx == '-' else 'asc' return ordering_fields - def get_query_set(self, request): + def get_queryset(self, request): # First, we collect all the declared list filters. (self.filter_specs, self.has_filters, remaining_lookup_params, use_distinct) = self.get_filters(request) # Then, we let every list filter modify the queryset to its liking. - qs = self.root_query_set + qs = self.root_queryset for filter_spec in self.filter_specs: new_qs = filter_spec.queryset(request, qs) if new_qs is not None: diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index 7b816674d3..0abc361a41 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -118,7 +118,7 @@ class UserAdmin(admin.ModelAdmin): def user_change_password(self, request, id, form_url=''): if not self.has_change_permission(request): raise PermissionDenied - user = get_object_or_404(self.queryset(request), pk=id) + user = get_object_or_404(self.get_queryset(request), pk=id) if request.method == 'POST': form = self.change_password_form(user, request.POST) if form.is_valid(): diff --git a/django/contrib/comments/managers.py b/django/contrib/comments/managers.py index bc0fc5f332..656200437b 100644 --- a/django/contrib/comments/managers.py +++ b/django/contrib/comments/managers.py @@ -8,7 +8,7 @@ class CommentManager(models.Manager): """ QuerySet for all comments currently in the moderation queue. """ - return self.get_query_set().filter(is_public=False, is_removed=False) + return self.get_queryset().filter(is_public=False, is_removed=False) def for_model(self, model): """ @@ -16,7 +16,7 @@ class CommentManager(models.Manager): a class). """ ct = ContentType.objects.get_for_model(model) - qs = self.get_query_set().filter(content_type=ct) + qs = self.get_queryset().filter(content_type=ct) if isinstance(model, models.Model): qs = qs.filter(object_pk=force_text(model._get_pk_val())) return qs diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index b5266d9bb3..d8eed76ad6 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -3,11 +3,20 @@ from django.template.loader import render_to_string from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.contrib import comments +from django.utils import six +from django.utils.deprecation import RenameMethodsBase from django.utils.encoding import smart_text register = template.Library() -class BaseCommentNode(template.Node): + +class RenameBaseCommentNodeMethods(RenameMethodsBase): + renamed_methods = ( + ('get_query_set', 'get_queryset', PendingDeprecationWarning), + ) + + +class BaseCommentNode(six.with_metaclass(RenameBaseCommentNodeMethods, template.Node)): """ Base helper class (abstract) for handling the get_comment_* template tags. Looks a bit strange, but the subclasses below should make this a bit more @@ -64,11 +73,11 @@ class BaseCommentNode(template.Node): self.comment = comment def render(self, context): - qs = self.get_query_set(context) + qs = self.get_queryset(context) context[self.as_varname] = self.get_context_value_from_queryset(context, qs) return '' - def get_query_set(self, context): + def get_queryset(self, context): ctype, object_pk = self.get_target_ctype_pk(context) if not object_pk: return self.comment_model.objects.none() @@ -205,7 +214,7 @@ class RenderCommentListNode(CommentListNode): "comments/%s/list.html" % ctype.app_label, "comments/list.html" ] - qs = self.get_query_set(context) + qs = self.get_queryset(context) context.push() liststr = render_to_string(template_search_list, { "comment_list" : self.get_context_value_from_queryset(context, qs) diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py index 20ff7042bc..fdb05a626a 100644 --- a/django/contrib/contenttypes/generic.py +++ b/django/contrib/contenttypes/generic.py @@ -16,10 +16,18 @@ from django.forms import ModelForm from django.forms.models import BaseModelFormSet, modelformset_factory, save_instance from django.contrib.admin.options import InlineModelAdmin, flatten_fieldsets from django.contrib.contenttypes.models import ContentType +from django.utils import six +from django.utils.deprecation import RenameMethodsBase from django.utils.encoding import smart_text -class GenericForeignKey(object): +class RenameGenericForeignKeyMethods(RenameMethodsBase): + renamed_methods = ( + ('get_prefetch_query_set', 'get_prefetch_queryset', PendingDeprecationWarning), + ) + + +class GenericForeignKey(six.with_metaclass(RenameGenericForeignKeyMethods)): """ Provides a generic relation to any object through content-type/object-id fields. @@ -60,7 +68,7 @@ class GenericForeignKey(object): # This should never happen. I love comments like this, don't you? raise Exception("Impossible arguments to GFK.get_content_type!") - def get_prefetch_query_set(self, instances): + def get_prefetch_queryset(self, instances): # For efficiency, group the instances by content type and then do one # query per model fk_dict = defaultdict(set) @@ -316,21 +324,21 @@ def create_generic_related_manager(superclass): '%s__exact' % object_id_field_name: instance._get_pk_val(), } - def get_query_set(self): + def get_queryset(self): try: return self.instance._prefetched_objects_cache[self.prefetch_cache_name] except (AttributeError, KeyError): db = self._db or router.db_for_read(self.model, instance=self.instance) - return super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**self.core_filters) + return super(GenericRelatedObjectManager, self).get_queryset().using(db).filter(**self.core_filters) - def get_prefetch_query_set(self, instances): + def get_prefetch_queryset(self, instances): db = self._db or router.db_for_read(self.model, instance=instances[0]) query = { '%s__pk' % self.content_type_field_name: self.content_type.id, '%s__in' % self.object_id_field_name: set(obj._get_pk_val() for obj in instances) } - qs = super(GenericRelatedObjectManager, self).get_query_set().using(db).filter(**query) + qs = super(GenericRelatedObjectManager, self).get_queryset().using(db).filter(**query) # We (possibly) need to convert object IDs to the type of the # instances' PK in order to match up instances: object_id_converter = instances[0]._meta.pk.to_python diff --git a/django/contrib/gis/db/models/manager.py b/django/contrib/gis/db/models/manager.py index 61fb82132b..aa57e3a507 100644 --- a/django/contrib/gis/db/models/manager.py +++ b/django/contrib/gis/db/models/manager.py @@ -9,95 +9,95 @@ class GeoManager(Manager): # properly. use_for_related_fields = True - def get_query_set(self): + def get_queryset(self): return GeoQuerySet(self.model, using=self._db) def area(self, *args, **kwargs): - return self.get_query_set().area(*args, **kwargs) + return self.get_queryset().area(*args, **kwargs) def centroid(self, *args, **kwargs): - return self.get_query_set().centroid(*args, **kwargs) + return self.get_queryset().centroid(*args, **kwargs) def collect(self, *args, **kwargs): - return self.get_query_set().collect(*args, **kwargs) + return self.get_queryset().collect(*args, **kwargs) def difference(self, *args, **kwargs): - return self.get_query_set().difference(*args, **kwargs) + return self.get_queryset().difference(*args, **kwargs) def distance(self, *args, **kwargs): - return self.get_query_set().distance(*args, **kwargs) + return self.get_queryset().distance(*args, **kwargs) def envelope(self, *args, **kwargs): - return self.get_query_set().envelope(*args, **kwargs) + return self.get_queryset().envelope(*args, **kwargs) def extent(self, *args, **kwargs): - return self.get_query_set().extent(*args, **kwargs) + return self.get_queryset().extent(*args, **kwargs) def extent3d(self, *args, **kwargs): - return self.get_query_set().extent3d(*args, **kwargs) + return self.get_queryset().extent3d(*args, **kwargs) def force_rhr(self, *args, **kwargs): - return self.get_query_set().force_rhr(*args, **kwargs) + return self.get_queryset().force_rhr(*args, **kwargs) def geohash(self, *args, **kwargs): - return self.get_query_set().geohash(*args, **kwargs) + return self.get_queryset().geohash(*args, **kwargs) def geojson(self, *args, **kwargs): - return self.get_query_set().geojson(*args, **kwargs) + return self.get_queryset().geojson(*args, **kwargs) def gml(self, *args, **kwargs): - return self.get_query_set().gml(*args, **kwargs) + return self.get_queryset().gml(*args, **kwargs) def intersection(self, *args, **kwargs): - return self.get_query_set().intersection(*args, **kwargs) + return self.get_queryset().intersection(*args, **kwargs) def kml(self, *args, **kwargs): - return self.get_query_set().kml(*args, **kwargs) + return self.get_queryset().kml(*args, **kwargs) def length(self, *args, **kwargs): - return self.get_query_set().length(*args, **kwargs) + return self.get_queryset().length(*args, **kwargs) def make_line(self, *args, **kwargs): - return self.get_query_set().make_line(*args, **kwargs) + return self.get_queryset().make_line(*args, **kwargs) def mem_size(self, *args, **kwargs): - return self.get_query_set().mem_size(*args, **kwargs) + return self.get_queryset().mem_size(*args, **kwargs) def num_geom(self, *args, **kwargs): - return self.get_query_set().num_geom(*args, **kwargs) + return self.get_queryset().num_geom(*args, **kwargs) def num_points(self, *args, **kwargs): - return self.get_query_set().num_points(*args, **kwargs) + return self.get_queryset().num_points(*args, **kwargs) def perimeter(self, *args, **kwargs): - return self.get_query_set().perimeter(*args, **kwargs) + return self.get_queryset().perimeter(*args, **kwargs) def point_on_surface(self, *args, **kwargs): - return self.get_query_set().point_on_surface(*args, **kwargs) + return self.get_queryset().point_on_surface(*args, **kwargs) def reverse_geom(self, *args, **kwargs): - return self.get_query_set().reverse_geom(*args, **kwargs) + return self.get_queryset().reverse_geom(*args, **kwargs) def scale(self, *args, **kwargs): - return self.get_query_set().scale(*args, **kwargs) + return self.get_queryset().scale(*args, **kwargs) def snap_to_grid(self, *args, **kwargs): - return self.get_query_set().snap_to_grid(*args, **kwargs) + return self.get_queryset().snap_to_grid(*args, **kwargs) def svg(self, *args, **kwargs): - return self.get_query_set().svg(*args, **kwargs) + return self.get_queryset().svg(*args, **kwargs) def sym_difference(self, *args, **kwargs): - return self.get_query_set().sym_difference(*args, **kwargs) + return self.get_queryset().sym_difference(*args, **kwargs) def transform(self, *args, **kwargs): - return self.get_query_set().transform(*args, **kwargs) + return self.get_queryset().transform(*args, **kwargs) def translate(self, *args, **kwargs): - return self.get_query_set().translate(*args, **kwargs) + return self.get_queryset().translate(*args, **kwargs) def union(self, *args, **kwargs): - return self.get_query_set().union(*args, **kwargs) + return self.get_queryset().union(*args, **kwargs) def unionagg(self, *args, **kwargs): - return self.get_query_set().unionagg(*args, **kwargs) + return self.get_queryset().unionagg(*args, **kwargs) diff --git a/django/contrib/sites/managers.py b/django/contrib/sites/managers.py index 3df485a040..becb35b404 100644 --- a/django/contrib/sites/managers.py +++ b/django/contrib/sites/managers.py @@ -35,7 +35,7 @@ class CurrentSiteManager(models.Manager): (self.__class__.__name__, self.__field_name, self.model._meta.object_name)) self.__is_validated = True - def get_query_set(self): + def get_queryset(self): if not self.__is_validated: self._validate_field_name() - return super(CurrentSiteManager, self).get_query_set().filter(**{self.__field_name + '__id__exact': settings.SITE_ID}) + return super(CurrentSiteManager, self).get_queryset().filter(**{self.__field_name + '__id__exact': settings.SITE_ID}) diff --git a/django/core/serializers/__init__.py b/django/core/serializers/__init__.py index cf7e66190f..c48050415d 100644 --- a/django/core/serializers/__init__.py +++ b/django/core/serializers/__init__.py @@ -4,7 +4,7 @@ Interfaces for serializing Django objects. Usage:: from django.core import serializers - json = serializers.serialize("json", some_query_set) + json = serializers.serialize("json", some_queryset) objects = list(serializers.deserialize("json", json)) To add your own serializers, use the SERIALIZATION_MODULES setting:: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 399d16600c..3b47eb86bb 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -11,6 +11,7 @@ from django.db.models.query_utils import QueryWrapper from django.db.models.deletion import CASCADE from django.utils.encoding import smart_text from django.utils import six +from django.utils.deprecation import RenameMethodsBase from django.utils.translation import ugettext_lazy as _, string_concat from django.utils.functional import curry, cached_property from django.core import exceptions @@ -225,7 +226,14 @@ class RelatedField(object): return self.rel.related_name or self.opts.model_name -class SingleRelatedObjectDescriptor(object): +class RenameRelatedObjectDescriptorMethods(RenameMethodsBase): + renamed_methods = ( + ('get_query_set', 'get_queryset', PendingDeprecationWarning), + ('get_prefetch_query_set', 'get_prefetch_queryset', PendingDeprecationWarning), + ) + + +class SingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescriptorMethods)): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have # a single "remote" value, on the class pointed to by a related field. @@ -238,16 +246,16 @@ class SingleRelatedObjectDescriptor(object): def is_cached(self, instance): return hasattr(instance, self.cache_name) - def get_query_set(self, **db_hints): + def get_queryset(self, **db_hints): db = router.db_for_read(self.related.model, **db_hints) return self.related.model._base_manager.using(db) - def get_prefetch_query_set(self, instances): + def get_prefetch_queryset(self, instances): rel_obj_attr = attrgetter(self.related.field.attname) instance_attr = lambda obj: obj._get_pk_val() instances_dict = dict((instance_attr(inst), inst) for inst in instances) params = {'%s__pk__in' % self.related.field.name: list(instances_dict)} - qs = self.get_query_set(instance=instances[0]).filter(**params) + qs = self.get_queryset(instance=instances[0]).filter(**params) # Since we're going to assign directly in the cache, # we must manage the reverse relation cache manually. rel_obj_cache_name = self.related.field.get_cache_name() @@ -268,7 +276,7 @@ class SingleRelatedObjectDescriptor(object): else: params = {'%s__pk' % self.related.field.name: related_pk} try: - rel_obj = self.get_query_set(instance=instance).get(**params) + rel_obj = self.get_queryset(instance=instance).get(**params) except self.related.model.DoesNotExist: rel_obj = None else: @@ -321,7 +329,7 @@ class SingleRelatedObjectDescriptor(object): setattr(value, self.related.field.get_cache_name(), instance) -class ReverseSingleRelatedObjectDescriptor(object): +class ReverseSingleRelatedObjectDescriptor(six.with_metaclass(RenameRelatedObjectDescriptorMethods)): # This class provides the functionality that makes the related-object # managers available as attributes on a model class, for fields that have # a single "remote" value, on the class that defines the related field. @@ -334,7 +342,7 @@ class ReverseSingleRelatedObjectDescriptor(object): def is_cached(self, instance): return hasattr(instance, self.cache_name) - def get_query_set(self, **db_hints): + def get_queryset(self, **db_hints): db = router.db_for_read(self.field.rel.to, **db_hints) rel_mgr = self.field.rel.to._default_manager # If the related manager indicates that it should be used for @@ -344,7 +352,7 @@ class ReverseSingleRelatedObjectDescriptor(object): else: return QuerySet(self.field.rel.to).using(db) - def get_prefetch_query_set(self, instances): + def get_prefetch_queryset(self, instances): other_field = self.field.rel.get_related_field() rel_obj_attr = attrgetter(other_field.attname) instance_attr = attrgetter(self.field.attname) @@ -353,7 +361,7 @@ class ReverseSingleRelatedObjectDescriptor(object): params = {'%s__pk__in' % self.field.rel.field_name: list(instances_dict)} else: params = {'%s__in' % self.field.rel.field_name: list(instances_dict)} - qs = self.get_query_set(instance=instances[0]).filter(**params) + qs = self.get_queryset(instance=instances[0]).filter(**params) # Since we're going to assign directly in the cache, # we must manage the reverse relation cache manually. if not self.field.rel.multiple: @@ -378,7 +386,7 @@ class ReverseSingleRelatedObjectDescriptor(object): params = {'%s__%s' % (self.field.rel.field_name, other_field.rel.field_name): val} else: params = {'%s__exact' % self.field.rel.field_name: val} - qs = self.get_query_set(instance=instance) + qs = self.get_queryset(instance=instance) # Assuming the database enforces foreign keys, this won't fail. rel_obj = qs.get(**params) if not self.field.rel.multiple: @@ -490,26 +498,26 @@ class ForeignRelatedObjectsDescriptor(object): } self.model = rel_model - def get_query_set(self): + def get_queryset(self): try: return self.instance._prefetched_objects_cache[rel_field.related_query_name()] except (AttributeError, KeyError): db = self._db or router.db_for_read(self.model, instance=self.instance) - qs = super(RelatedManager, self).get_query_set().using(db).filter(**self.core_filters) + qs = super(RelatedManager, self).get_queryset().using(db).filter(**self.core_filters) val = getattr(self.instance, attname) if val is None or val == '' and connections[db].features.interprets_empty_strings_as_nulls: return qs.none() qs._known_related_objects = {rel_field: {self.instance.pk: self.instance}} return qs - def get_prefetch_query_set(self, instances): + def get_prefetch_queryset(self, instances): rel_obj_attr = attrgetter(rel_field.attname) instance_attr = attrgetter(attname) instances_dict = dict((instance_attr(inst), inst) for inst in instances) db = self._db or router.db_for_read(self.model, instance=instances[0]) query = {'%s__%s__in' % (rel_field.name, attname): list(instances_dict)} - qs = super(RelatedManager, self).get_query_set().using(db).filter(**query) - # Since we just bypassed this class' get_query_set(), we must manage + qs = super(RelatedManager, self).get_queryset().using(db).filter(**query) + # Since we just bypassed this class' get_queryset(), we must manage # the reverse relation manually. for rel_obj in qs: instance = instances_dict[rel_obj_attr(rel_obj)] @@ -603,20 +611,20 @@ def create_many_related_manager(superclass, rel): else: return obj.pk - def get_query_set(self): + def get_queryset(self): try: return self.instance._prefetched_objects_cache[self.prefetch_cache_name] except (AttributeError, KeyError): db = self._db or router.db_for_read(self.instance.__class__, instance=self.instance) - return super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**self.core_filters) + return super(ManyRelatedManager, self).get_queryset().using(db)._next_is_sticky().filter(**self.core_filters) - def get_prefetch_query_set(self, instances): + def get_prefetch_queryset(self, instances): instance = instances[0] from django.db import connections db = self._db or router.db_for_read(instance.__class__, instance=instance) query = {'%s__pk__in' % self.query_field_name: set(obj._get_pk_val() for obj in instances)} - qs = super(ManyRelatedManager, self).get_query_set().using(db)._next_is_sticky().filter(**query) + qs = super(ManyRelatedManager, self).get_queryset().using(db)._next_is_sticky().filter(**query) # M2M: need to annotate the query in order to get the primary model # that the secondary model was actually related to. We know that diff --git a/django/db/models/manager.py b/django/db/models/manager.py index b1f2e10735..43a8264f11 100644 --- a/django/db/models/manager.py +++ b/django/db/models/manager.py @@ -3,7 +3,8 @@ from django.db import router from django.db.models.query import QuerySet, insert_query, RawQuerySet from django.db.models import signals from django.db.models.fields import FieldDoesNotExist - +from django.utils import six +from django.utils.deprecation import RenameMethodsBase def ensure_default_manager(sender, **kwargs): """ @@ -47,7 +48,14 @@ def ensure_default_manager(sender, **kwargs): signals.class_prepared.connect(ensure_default_manager) -class Manager(object): +class RenameManagerMethods(RenameMethodsBase): + renamed_methods = ( + ('get_query_set', 'get_queryset', PendingDeprecationWarning), + ('get_prefetch_query_set', 'get_prefetch_queryset', PendingDeprecationWarning), + ) + + +class Manager(six.with_metaclass(RenameManagerMethods)): # Tracks each time a Manager instance is created. Used to retain order. creation_counter = 0 @@ -112,113 +120,113 @@ class Manager(object): # PROXIES TO QUERYSET # ####################### - def get_query_set(self): + def get_queryset(self): """Returns a new QuerySet object. Subclasses can override this method to easily customize the behavior of the Manager. """ return QuerySet(self.model, using=self._db) def none(self): - return self.get_query_set().none() + return self.get_queryset().none() def all(self): - return self.get_query_set() + return self.get_queryset() def count(self): - return self.get_query_set().count() + return self.get_queryset().count() def dates(self, *args, **kwargs): - return self.get_query_set().dates(*args, **kwargs) + return self.get_queryset().dates(*args, **kwargs) def datetimes(self, *args, **kwargs): - return self.get_query_set().datetimes(*args, **kwargs) + return self.get_queryset().datetimes(*args, **kwargs) def distinct(self, *args, **kwargs): - return self.get_query_set().distinct(*args, **kwargs) + return self.get_queryset().distinct(*args, **kwargs) def extra(self, *args, **kwargs): - return self.get_query_set().extra(*args, **kwargs) + return self.get_queryset().extra(*args, **kwargs) def get(self, *args, **kwargs): - return self.get_query_set().get(*args, **kwargs) + return self.get_queryset().get(*args, **kwargs) def get_or_create(self, **kwargs): - return self.get_query_set().get_or_create(**kwargs) + return self.get_queryset().get_or_create(**kwargs) def create(self, **kwargs): - return self.get_query_set().create(**kwargs) + return self.get_queryset().create(**kwargs) def bulk_create(self, *args, **kwargs): - return self.get_query_set().bulk_create(*args, **kwargs) + return self.get_queryset().bulk_create(*args, **kwargs) def filter(self, *args, **kwargs): - return self.get_query_set().filter(*args, **kwargs) + return self.get_queryset().filter(*args, **kwargs) def aggregate(self, *args, **kwargs): - return self.get_query_set().aggregate(*args, **kwargs) + return self.get_queryset().aggregate(*args, **kwargs) def annotate(self, *args, **kwargs): - return self.get_query_set().annotate(*args, **kwargs) + return self.get_queryset().annotate(*args, **kwargs) def complex_filter(self, *args, **kwargs): - return self.get_query_set().complex_filter(*args, **kwargs) + return self.get_queryset().complex_filter(*args, **kwargs) def exclude(self, *args, **kwargs): - return self.get_query_set().exclude(*args, **kwargs) + return self.get_queryset().exclude(*args, **kwargs) def in_bulk(self, *args, **kwargs): - return self.get_query_set().in_bulk(*args, **kwargs) + return self.get_queryset().in_bulk(*args, **kwargs) def iterator(self, *args, **kwargs): - return self.get_query_set().iterator(*args, **kwargs) + return self.get_queryset().iterator(*args, **kwargs) def earliest(self, *args, **kwargs): - return self.get_query_set().earliest(*args, **kwargs) + return self.get_queryset().earliest(*args, **kwargs) def latest(self, *args, **kwargs): - return self.get_query_set().latest(*args, **kwargs) + return self.get_queryset().latest(*args, **kwargs) def order_by(self, *args, **kwargs): - return self.get_query_set().order_by(*args, **kwargs) + return self.get_queryset().order_by(*args, **kwargs) def select_for_update(self, *args, **kwargs): - return self.get_query_set().select_for_update(*args, **kwargs) + return self.get_queryset().select_for_update(*args, **kwargs) def select_related(self, *args, **kwargs): - return self.get_query_set().select_related(*args, **kwargs) + return self.get_queryset().select_related(*args, **kwargs) def prefetch_related(self, *args, **kwargs): - return self.get_query_set().prefetch_related(*args, **kwargs) + return self.get_queryset().prefetch_related(*args, **kwargs) def values(self, *args, **kwargs): - return self.get_query_set().values(*args, **kwargs) + return self.get_queryset().values(*args, **kwargs) def values_list(self, *args, **kwargs): - return self.get_query_set().values_list(*args, **kwargs) + return self.get_queryset().values_list(*args, **kwargs) def update(self, *args, **kwargs): - return self.get_query_set().update(*args, **kwargs) + return self.get_queryset().update(*args, **kwargs) def reverse(self, *args, **kwargs): - return self.get_query_set().reverse(*args, **kwargs) + return self.get_queryset().reverse(*args, **kwargs) def defer(self, *args, **kwargs): - return self.get_query_set().defer(*args, **kwargs) + return self.get_queryset().defer(*args, **kwargs) def only(self, *args, **kwargs): - return self.get_query_set().only(*args, **kwargs) + return self.get_queryset().only(*args, **kwargs) def using(self, *args, **kwargs): - return self.get_query_set().using(*args, **kwargs) + return self.get_queryset().using(*args, **kwargs) def exists(self, *args, **kwargs): - return self.get_query_set().exists(*args, **kwargs) + return self.get_queryset().exists(*args, **kwargs) def _insert(self, objs, fields, **kwargs): return insert_query(self.model, objs, fields, **kwargs) def _update(self, values, **kwargs): - return self.get_query_set()._update(values, **kwargs) + return self.get_queryset()._update(values, **kwargs) def raw(self, raw_query, params=None, *args, **kwargs): return RawQuerySet(raw_query=raw_query, model=self.model, params=params, using=self._db, *args, **kwargs) @@ -265,5 +273,5 @@ class EmptyManager(Manager): super(EmptyManager, self).__init__() self.model = model - def get_query_set(self): - return super(EmptyManager, self).get_query_set().none() + def get_queryset(self): + return super(EmptyManager, self).get_queryset().none() diff --git a/django/db/models/query.py b/django/db/models/query.py index ec35f8aba3..30be30ca43 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -1733,9 +1733,9 @@ def prefetch_related_objects(result_cache, related_lookups): def get_prefetcher(instance, attr): """ For the attribute 'attr' on the given instance, finds - an object that has a get_prefetch_query_set(). + an object that has a get_prefetch_queryset(). Returns a 4 tuple containing: - (the object with get_prefetch_query_set (or None), + (the object with get_prefetch_queryset (or None), the descriptor object representing this relationship (or None), a boolean that is False if the attribute was not found at all, a boolean that is True if the attribute has already been fetched) @@ -1758,8 +1758,8 @@ def get_prefetcher(instance, attr): attr_found = True if rel_obj_descriptor: # singly related object, descriptor object has the - # get_prefetch_query_set() method. - if hasattr(rel_obj_descriptor, 'get_prefetch_query_set'): + # get_prefetch_queryset() method. + if hasattr(rel_obj_descriptor, 'get_prefetch_queryset'): prefetcher = rel_obj_descriptor if rel_obj_descriptor.is_cached(instance): is_fetched = True @@ -1768,7 +1768,7 @@ def get_prefetcher(instance, attr): # the attribute on the instance rather than the class to # support many related managers rel_obj = getattr(instance, attr) - if hasattr(rel_obj, 'get_prefetch_query_set'): + if hasattr(rel_obj, 'get_prefetch_queryset'): prefetcher = rel_obj return prefetcher, rel_obj_descriptor, attr_found, is_fetched @@ -1784,7 +1784,7 @@ def prefetch_one_level(instances, prefetcher, attname): prefetches that must be done due to prefetch_related lookups found from default managers. """ - # prefetcher must have a method get_prefetch_query_set() which takes a list + # prefetcher must have a method get_prefetch_queryset() which takes a list # of instances, and returns a tuple: # (queryset of instances of self.model that are related to passed in instances, @@ -1797,7 +1797,7 @@ def prefetch_one_level(instances, prefetcher, attname): # in a dictionary. rel_qs, rel_obj_attr, instance_attr, single, cache_name =\ - prefetcher.get_prefetch_query_set(instances) + prefetcher.get_prefetch_queryset(instances) # We have to handle the possibility that the default manager itself added # prefetch_related lookups to the QuerySet we just got back. We don't want to # trigger the prefetch_related functionality by evaluating the query. diff --git a/django/forms/models.py b/django/forms/models.py index 7609bb7227..272f1ddee6 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -478,7 +478,7 @@ class BaseModelFormSet(BaseFormSet): if self.queryset is not None: qs = self.queryset else: - qs = self.model._default_manager.get_query_set() + qs = self.model._default_manager.get_queryset() # If the queryset isn't already ordered we need to add an # artificial ordering here to make sure that all formsets @@ -668,9 +668,9 @@ class BaseModelFormSet(BaseFormSet): except IndexError: pk_value = None if isinstance(pk, OneToOneField) or isinstance(pk, ForeignKey): - qs = pk.rel.to._default_manager.get_query_set() + qs = pk.rel.to._default_manager.get_queryset() else: - qs = self.model._default_manager.get_query_set() + qs = self.model._default_manager.get_queryset() qs = qs.using(form.instance._state.db) if form._meta.widgets: widget = form._meta.widgets.get(self._pk_field.name, HiddenInput) diff --git a/django/utils/deprecation.py b/django/utils/deprecation.py new file mode 100644 index 0000000000..edbb5ca5ea --- /dev/null +++ b/django/utils/deprecation.py @@ -0,0 +1,62 @@ +import inspect +import warnings + + +class warn_about_renamed_method(object): + def __init__(self, class_name, old_method_name, new_method_name, deprecation_warning): + self.class_name = class_name + self.old_method_name = old_method_name + self.new_method_name = new_method_name + self.deprecation_warning = deprecation_warning + + def __call__(self, f): + def wrapped(*args, **kwargs): + warnings.warn( + "`%s.%s` is deprecated, use `%s` instead." % + (self.class_name, self.old_method_name, self.new_method_name), + self.deprecation_warning, 2) + return f(*args, **kwargs) + return wrapped + + +class RenameMethodsBase(type): + """ + Handles the deprecation paths when renaming a method. + + It does the following: + 1) Define the new method if missing and complain about it. + 2) Define the old method if missing. + 3) Complain whenever an old method is called. + + See #15363 for more details. + """ + + renamed_methods = () + + def __new__(cls, name, bases, attrs): + new_class = super(RenameMethodsBase, cls).__new__(cls, name, bases, attrs) + + for base in inspect.getmro(new_class): + class_name = base.__name__ + for renamed_method in cls.renamed_methods: + old_method_name = renamed_method[0] + old_method = base.__dict__.get(old_method_name) + new_method_name = renamed_method[1] + new_method = base.__dict__.get(new_method_name) + deprecation_warning = renamed_method[2] + wrapper = warn_about_renamed_method(class_name, *renamed_method) + + # Define the new method if missing and complain about it + if not new_method and old_method: + warnings.warn( + "`%s.%s` method should be renamed `%s`." % + (class_name, old_method_name, new_method_name), + deprecation_warning, 2) + setattr(base, new_method_name, old_method) + setattr(base, old_method_name, wrapper(old_method)) + + # Define the old method as a wrapped call to the new method. + if not old_method and new_method: + setattr(base, old_method_name, wrapper(new_method)) + + return new_class diff --git a/docs/faq/admin.txt b/docs/faq/admin.txt index 30d452cbe2..1d9a7c7427 100644 --- a/docs/faq/admin.txt +++ b/docs/faq/admin.txt @@ -49,7 +49,7 @@ How do I limit admin access so that objects can only be edited by the users who The :class:`~django.contrib.admin.ModelAdmin` class also provides customization hooks that allow you to control the visibility and editability of objects in the admin. Using the same trick of extracting the user from the request, the -:meth:`~django.contrib.admin.ModelAdmin.queryset` and +:meth:`~django.contrib.admin.ModelAdmin.get_queryset` and :meth:`~django.contrib.admin.ModelAdmin.has_change_permission` can be used to control the visibility and editability of objects in the admin. diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 3a9cbd195d..b5173af298 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -341,6 +341,15 @@ these changes. * The private API ``django.db.close_connection`` will be removed. +* Remove the backward compatible shims introduced to rename ``get_query_set`` + and similar queryset methods. This affects the following classes: + ``BaseModelAdmin``, ``ChangeList``, ``BaseCommentNode``, + ``GenericForeignKey``, ``Manager``, ``SingleRelatedObjectDescriptor`` and + ``ReverseSingleRelatedObjectDescriptor``. + +* Remove the backward compatible shims introduced to rename the attributes + ``ChangeList.root_query_set`` and ``ChangeList.query_set``. + 2.0 --- diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt index 9a0f3ca7f8..ae2ee44601 100644 --- a/docs/ref/contrib/admin/index.txt +++ b/docs/ref/contrib/admin/index.txt @@ -703,7 +703,7 @@ subclass:: Only show the lookups if there actually is anyone born in the corresponding decades. """ - qs = model_admin.queryset(request) + qs = model_admin.get_queryset(request) if qs.filter(birthday__gte=date(1980, 1, 1), birthday__lte=date(1989, 12, 31)).exists(): yield ('80s', _('in the eighties')) @@ -1326,20 +1326,23 @@ templates used by the :class:`ModelAdmin` views: be interpreted as meaning that the current user is not permitted to delete any object of this type). -.. method:: ModelAdmin.queryset(self, request) +.. method:: ModelAdmin.get_queryset(self, request) - The ``queryset`` method on a ``ModelAdmin`` returns a + The ``get_queryset`` method on a ``ModelAdmin`` returns a :class:`~django.db.models.query.QuerySet` of all model instances that can be edited by the admin site. One use case for overriding this method is to show objects owned by the logged-in user:: class MyModelAdmin(admin.ModelAdmin): - def queryset(self, request): - qs = super(MyModelAdmin, self).queryset(request) + def get_queryset(self, request): + qs = super(MyModelAdmin, self).get_queryset(request) if request.user.is_superuser: return qs return qs.filter(author=request.user) + .. versionchanged:: 1.6 + The ``get_queryset`` method was previously named ``queryset``. + .. method:: ModelAdmin.message_user(request, message, level=messages.INFO, extra_tags='', fail_silently=False) Sends a message to the user using the :mod:`django.contrib.messages` @@ -1549,7 +1552,7 @@ adds some of its own (the shared features are actually defined in the - :attr:`~ModelAdmin.filter_vertical` - :attr:`~ModelAdmin.ordering` - :attr:`~ModelAdmin.prepopulated_fields` -- :meth:`~ModelAdmin.queryset` +- :meth:`~ModelAdmin.get_queryset` - :attr:`~ModelAdmin.radio_fields` - :attr:`~ModelAdmin.readonly_fields` - :attr:`~InlineModelAdmin.raw_id_fields` diff --git a/docs/ref/models/querysets.txt b/docs/ref/models/querysets.txt index 0fa8b8e361..224c2427b0 100644 --- a/docs/ref/models/querysets.txt +++ b/docs/ref/models/querysets.txt @@ -1586,32 +1586,32 @@ The most efficient method of finding whether a model with a unique field (e.g. ``primary_key``) is a member of a :class:`.QuerySet` is:: entry = Entry.objects.get(pk=123) - if some_query_set.filter(pk=entry.pk).exists(): + if some_queryset.filter(pk=entry.pk).exists(): print("Entry contained in queryset") Which will be faster than the following which requires evaluating and iterating through the entire queryset:: - if entry in some_query_set: + if entry in some_queryset: print("Entry contained in QuerySet") And to find whether a queryset contains any items:: - if some_query_set.exists(): - print("There is at least one object in some_query_set") + if some_queryset.exists(): + print("There is at least one object in some_queryset") Which will be faster than:: - if some_query_set: - print("There is at least one object in some_query_set") + if some_queryset: + print("There is at least one object in some_queryset") ... but not by a large degree (hence needing a large queryset for efficiency gains). -Additionally, if a ``some_query_set`` has not yet been evaluated, but you know -that it will be at some point, then using ``some_query_set.exists()`` will do +Additionally, if a ``some_queryset`` has not yet been evaluated, but you know +that it will be at some point, then using ``some_queryset.exists()`` will do more overall work (one query for the existence check plus an extra one to later -retrieve the results) than simply using ``bool(some_query_set)``, which +retrieve the results) than simply using ``bool(some_queryset)``, which retrieves the results and then checks if any were returned. update diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 81b1e48d25..c8012ab7c2 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -289,3 +289,9 @@ on a widget, you should now define this method on the form field itself. ``Model._meta.module_name`` was renamed to ``model_name``. Despite being a private API, it will go through a regular deprecation path. + +``get_query_set`` and similar methods renamed to ``get_queryset`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Methods that return a ``QuerySet`` such as ``Manager.get_query_set`` or +``ModelAdmin.queryset`` have been renamed to ``get_queryset``. diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index a14616a17c..a8c0d17076 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -108,7 +108,7 @@ example, using this model:: ...the statement ``Book.objects.all()`` will return all books in the database. You can override a ``Manager``\'s base ``QuerySet`` by overriding the -``Manager.get_query_set()`` method. ``get_query_set()`` should return a +``Manager.get_queryset()`` method. ``get_queryset()`` should return a ``QuerySet`` with the properties you require. For example, the following model has *two* ``Manager``\s -- one that returns @@ -116,8 +116,8 @@ all objects, and one that returns only the books by Roald Dahl:: # First, define the Manager subclass. class DahlBookManager(models.Manager): - def get_query_set(self): - return super(DahlBookManager, self).get_query_set().filter(author='Roald Dahl') + def get_queryset(self): + return super(DahlBookManager, self).get_queryset().filter(author='Roald Dahl') # Then hook it into the Book model explicitly. class Book(models.Model): @@ -131,7 +131,7 @@ With this sample model, ``Book.objects.all()`` will return all books in the database, but ``Book.dahl_objects.all()`` will only return the ones written by Roald Dahl. -Of course, because ``get_query_set()`` returns a ``QuerySet`` object, you can +Of course, because ``get_queryset()`` returns a ``QuerySet`` object, you can use ``filter()``, ``exclude()`` and all the other ``QuerySet`` methods on it. So these statements are all legal:: @@ -147,12 +147,12 @@ models. For example:: class MaleManager(models.Manager): - def get_query_set(self): - return super(MaleManager, self).get_query_set().filter(sex='M') + def get_queryset(self): + return super(MaleManager, self).get_queryset().filter(sex='M') class FemaleManager(models.Manager): - def get_query_set(self): - return super(FemaleManager, self).get_query_set().filter(sex='F') + def get_queryset(self): + return super(FemaleManager, self).get_queryset().filter(sex='F') class Person(models.Model): first_name = models.CharField(max_length=50) @@ -172,9 +172,12 @@ the "default" ``Manager``, and several parts of Django (including :djadmin:`dumpdata`) will use that ``Manager`` exclusively for that model. As a result, it's a good idea to be careful in your choice of default manager in order to avoid a situation where overriding -``get_query_set()`` results in an inability to retrieve objects you'd like to +``get_queryset()`` results in an inability to retrieve objects you'd like to work with. +.. versionchanged:: 1.6 + The ``get_queryset`` method was previously named ``get_query_set``. + .. _managers-for-related-objects: Using managers for related object access @@ -379,9 +382,9 @@ to from some other model. In those situations, Django has to be able to see all the objects for the model it is fetching, so that *anything* which is referred to can be retrieved. -If you override the ``get_query_set()`` method and filter out any rows, Django +If you override the ``get_queryset()`` method and filter out any rows, Django will return incorrect results. Don't do that. A manager that filters results -in ``get_query_set()`` is not appropriate for use as an automatic manager. +in ``get_queryset()`` is not appropriate for use as an automatic manager. Set ``use_for_related_fields`` when you define the class ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/multi-db.txt b/docs/topics/db/multi-db.txt index 8150e498de..ae23c3d9f3 100644 --- a/docs/topics/db/multi-db.txt +++ b/docs/topics/db/multi-db.txt @@ -506,19 +506,19 @@ solution is to use ``db_manager()``, like this:: ``db_manager()`` returns a copy of the manager bound to the database you specify. -Using ``get_query_set()`` with multiple databases +Using ``get_queryset()`` with multiple databases ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're overriding ``get_query_set()`` on your manager, be sure to +If you're overriding ``get_queryset()`` on your manager, be sure to either call the method on the parent (using ``super()``) or do the appropriate handling of the ``_db`` attribute on the manager (a string containing the name of the database to use). For example, if you want to return a custom ``QuerySet`` class from -the ``get_query_set`` method, you could do this:: +the ``get_queryset`` method, you could do this:: class MyManager(models.Manager): - def get_query_set(self): + def get_queryset(self): qs = CustomQuerySet(self.model) if self._db is not None: qs = qs.using(self._db) @@ -548,9 +548,9 @@ multiple-database support:: # Tell Django to delete objects from the 'other' database obj.delete(using=self.using) - def queryset(self, request): + def get_queryset(self, request): # Tell Django to look for objects on the 'other' database. - return super(MultiDBModelAdmin, self).queryset(request).using(self.using) + return super(MultiDBModelAdmin, self).get_queryset(request).using(self.using) def formfield_for_foreignkey(self, db_field, request=None, **kwargs): # Tell Django to populate ForeignKey widgets using a query @@ -573,9 +573,9 @@ Inlines can be handled in a similar fashion. They require three customized metho class MultiDBTabularInline(admin.TabularInline): using = 'other' - def queryset(self, request): + def get_queryset(self, request): # Tell Django to look for inline objects on the 'other' database. - return super(MultiDBTabularInline, self).queryset(request).using(self.using) + return super(MultiDBTabularInline, self).get_queryset(request).using(self.using) def formfield_for_foreignkey(self, db_field, request=None, **kwargs): # Tell Django to populate ForeignKey widgets using a query diff --git a/tests/admin_changelist/admin.py b/tests/admin_changelist/admin.py index 5751d04bce..8387ba77a1 100644 --- a/tests/admin_changelist/admin.py +++ b/tests/admin_changelist/admin.py @@ -34,8 +34,8 @@ class ChildAdmin(admin.ModelAdmin): list_per_page = 10 list_filter = ['parent', 'age'] - def queryset(self, request): - return super(ChildAdmin, self).queryset(request).select_related("parent__name") + def get_queryset(self, request): + return super(ChildAdmin, self).get_queryset(request).select_related("parent__name") class CustomPaginationAdmin(ChildAdmin): @@ -46,8 +46,8 @@ class FilteredChildAdmin(admin.ModelAdmin): list_display = ['name', 'parent'] list_per_page = 10 - def queryset(self, request): - return super(FilteredChildAdmin, self).queryset(request).filter( + def get_queryset(self, request): + return super(FilteredChildAdmin, self).get_queryset(request).filter( name__contains='filtered') diff --git a/tests/admin_changelist/models.py b/tests/admin_changelist/models.py index 4ba2f9c503..786b4385aa 100644 --- a/tests/admin_changelist/models.py +++ b/tests/admin_changelist/models.py @@ -74,8 +74,8 @@ class UnorderedObject(models.Model): class OrderedObjectManager(models.Manager): - def get_query_set(self): - return super(OrderedObjectManager, self).get_query_set().order_by('number') + def get_queryset(self): + return super(OrderedObjectManager, self).get_queryset().order_by('number') class OrderedObject(models.Model): """ diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py index bb39f22411..05cdcdb73d 100644 --- a/tests/admin_changelist/tests.py +++ b/tests/admin_changelist/tests.py @@ -39,15 +39,15 @@ class ChangeListTests(TestCase): def test_select_related_preserved(self): """ - Regression test for #10348: ChangeList.get_query_set() shouldn't - overwrite a custom select_related provided by ModelAdmin.queryset(). + Regression test for #10348: ChangeList.get_queryset() shouldn't + overwrite a custom select_related provided by ModelAdmin.get_queryset(). """ m = ChildAdmin(Child, admin.site) request = self.factory.get('/child/') cl = ChangeList(request, Child, m.list_display, m.list_display_links, m.list_filter, m.date_hierarchy, m.search_fields, m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m) - self.assertEqual(cl.query_set.query.select_related, {'parent': {'name': {}}}) + self.assertEqual(cl.queryset.query.select_related, {'parent': {'name': {}}}) def test_result_list_empty_changelist_value(self): """ @@ -277,7 +277,7 @@ class ChangeListTests(TestCase): m.list_max_show_all, m.list_editable, m) # Make sure distinct() was called - self.assertEqual(cl.query_set.count(), 1) + self.assertEqual(cl.queryset.count(), 1) def test_distinct_for_non_unique_related_object_in_search_fields(self): """ @@ -297,7 +297,7 @@ class ChangeListTests(TestCase): m.list_max_show_all, m.list_editable, m) # Make sure distinct() was called - self.assertEqual(cl.query_set.count(), 1) + self.assertEqual(cl.queryset.count(), 1) def test_pagination(self): """ @@ -317,7 +317,7 @@ class ChangeListTests(TestCase): m.list_filter, m.date_hierarchy, m.search_fields, m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m) - self.assertEqual(cl.query_set.count(), 60) + self.assertEqual(cl.queryset.count(), 60) self.assertEqual(cl.paginator.count, 60) self.assertEqual(list(cl.paginator.page_range), [1, 2, 3, 4, 5, 6]) @@ -327,7 +327,7 @@ class ChangeListTests(TestCase): m.list_filter, m.date_hierarchy, m.search_fields, m.list_select_related, m.list_per_page, m.list_max_show_all, m.list_editable, m) - self.assertEqual(cl.query_set.count(), 30) + self.assertEqual(cl.queryset.count(), 30) self.assertEqual(cl.paginator.count, 30) self.assertEqual(list(cl.paginator.page_range), [1, 2, 3]) diff --git a/tests/admin_filters/tests.py b/tests/admin_filters/tests.py index 11f792e07a..f05e8e2011 100644 --- a/tests/admin_filters/tests.py +++ b/tests/admin_filters/tests.py @@ -61,7 +61,7 @@ class DecadeListFilterWithFailingQueryset(DecadeListFilterWithTitleAndParameter) class DecadeListFilterWithQuerysetBasedLookups(DecadeListFilterWithTitleAndParameter): def lookups(self, request, model_admin): - qs = model_admin.queryset(request) + qs = model_admin.get_queryset(request) if qs.filter(year__gte=1980, year__lte=1989).exists(): yield ('the 80s', "the 1980's") if qs.filter(year__gte=1990, year__lte=1999).exists(): @@ -86,7 +86,7 @@ class DepartmentListFilterLookupWithNonStringValue(SimpleListFilter): return sorted(set([ (employee.department.id, # Intentionally not a string (Refs #19318) employee.department.code) - for employee in model_admin.queryset(request).all() + for employee in model_admin.get_queryset(request).all() ])) def queryset(self, request, queryset): @@ -183,7 +183,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book]) # Make sure the correct choice is selected @@ -200,7 +200,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) if (self.today.year, self.today.month) == (self.one_week_ago.year, self.one_week_ago.month): # In case one week ago is in the same month. self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book]) @@ -221,7 +221,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) if self.today.year == self.one_week_ago.year: # In case one week ago is in the same year. self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book]) @@ -242,7 +242,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book]) # Make sure the correct choice is selected @@ -266,7 +266,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.django_book]) # Make sure the last choice is None and is selected @@ -293,7 +293,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.gipsy_book]) # Make sure the last choice is None and is selected @@ -321,7 +321,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.django_book, self.bio_book, self.djangonaut_book]) # Make sure the last choice is None and is selected @@ -349,7 +349,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, User, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.lisa]) # Make sure the last choice is None and is selected @@ -374,7 +374,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, User, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.alfred]) # Make sure the last choice is None and is selected @@ -410,7 +410,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.bio_book]) # Make sure the correct choice is selected @@ -424,7 +424,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book]) # Make sure the correct choice is selected @@ -438,7 +438,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.django_book]) # Make sure the correct choice is selected @@ -457,7 +457,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id'))) # Make sure the correct choice is selected @@ -474,7 +474,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), []) # Make sure the correct choice is selected @@ -491,7 +491,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.bio_book]) # Make sure the correct choice is selected @@ -508,7 +508,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book]) # Make sure the correct choice is selected @@ -525,7 +525,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.djangonaut_book]) # Make sure the correct choices are selected @@ -615,7 +615,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.bio_book]) filterspec = changelist.get_filters(request)[0][-1] @@ -637,7 +637,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.bio_book]) # Make sure the correct choice is selected @@ -654,7 +654,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Book, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.bio_book]) # Make sure the correct choice is selected @@ -676,7 +676,7 @@ class ListFiltersTests(TestCase): request = self.request_factory.get('/', {'department': self.john.pk}) changelist = self.get_changelist(request, Employee, modeladmin) - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.john]) @@ -698,7 +698,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Employee, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.jack, self.john]) filterspec = changelist.get_filters(request)[0][-1] @@ -723,7 +723,7 @@ class ListFiltersTests(TestCase): changelist = self.get_changelist(request, Employee, modeladmin) # Make sure the correct queryset is returned - queryset = changelist.get_query_set(request) + queryset = changelist.get_queryset(request) self.assertEqual(list(queryset), [self.john]) filterspec = changelist.get_filters(request)[0][-1] diff --git a/tests/admin_ordering/tests.py b/tests/admin_ordering/tests.py index 10faa9533f..6655ad37ad 100644 --- a/tests/admin_ordering/tests.py +++ b/tests/admin_ordering/tests.py @@ -22,8 +22,8 @@ request.user = MockSuperUser() class TestAdminOrdering(TestCase): """ - Let's make sure that ModelAdmin.queryset uses the ordering we define in - ModelAdmin rather that ordering defined in the model's inner Meta + Let's make sure that ModelAdmin.get_queryset uses the ordering we define + in ModelAdmin rather that ordering defined in the model's inner Meta class. """ @@ -42,7 +42,7 @@ class TestAdminOrdering(TestCase): class. """ ma = ModelAdmin(Band, None) - names = [b.name for b in ma.queryset(request)] + names = [b.name for b in ma.get_queryset(request)] self.assertEqual(['Aerosmith', 'Radiohead', 'Van Halen'], names) def test_specified_ordering(self): @@ -53,7 +53,7 @@ class TestAdminOrdering(TestCase): class BandAdmin(ModelAdmin): ordering = ('rank',) # default ordering is ('name',) ma = BandAdmin(Band, None) - names = [b.name for b in ma.queryset(request)] + names = [b.name for b in ma.get_queryset(request)] self.assertEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names) def test_dynamic_ordering(self): @@ -65,17 +65,17 @@ class TestAdminOrdering(TestCase): request = self.request_factory.get('/') request.user = super_user ma = DynOrderingBandAdmin(Band, None) - names = [b.name for b in ma.queryset(request)] + names = [b.name for b in ma.get_queryset(request)] self.assertEqual(['Radiohead', 'Van Halen', 'Aerosmith'], names) request.user = other_user - names = [b.name for b in ma.queryset(request)] + names = [b.name for b in ma.get_queryset(request)] self.assertEqual(['Aerosmith', 'Radiohead', 'Van Halen'], names) class TestInlineModelAdminOrdering(TestCase): """ - Let's make sure that InlineModelAdmin.queryset uses the ordering we define - in InlineModelAdmin. + Let's make sure that InlineModelAdmin.get_queryset uses the ordering we + define in InlineModelAdmin. """ def setUp(self): @@ -95,7 +95,7 @@ class TestInlineModelAdminOrdering(TestCase): class. """ inline = SongInlineDefaultOrdering(self.b, None) - names = [s.name for s in inline.queryset(request)] + names = [s.name for s in inline.get_queryset(request)] self.assertEqual(['Dude (Looks Like a Lady)', 'Jaded', 'Pink'], names) def test_specified_ordering(self): @@ -103,7 +103,7 @@ class TestInlineModelAdminOrdering(TestCase): Let's check with ordering set to something different than the default. """ inline = SongInlineNewOrdering(self.b, None) - names = [s.name for s in inline.queryset(request)] + names = [s.name for s in inline.get_queryset(request)] self.assertEqual(['Jaded', 'Pink', 'Dude (Looks Like a Lady)'], names) diff --git a/tests/admin_views/admin.py b/tests/admin_views/admin.py index d4348968e0..cc7585cd2d 100644 --- a/tests/admin_views/admin.py +++ b/tests/admin_views/admin.py @@ -177,10 +177,10 @@ class PersonAdmin(admin.ModelAdmin): return super(PersonAdmin, self).get_changelist_formset(request, formset=BasePersonModelFormSet, **kwargs) - def queryset(self, request): + def get_queryset(self, request): # Order by a field that isn't in list display, to be able to test # whether ordering is preserved. - return super(PersonAdmin, self).queryset(request).order_by('age') + return super(PersonAdmin, self).get_queryset(request).order_by('age') class FooAccount(Account): @@ -283,8 +283,8 @@ class ParentAdmin(admin.ModelAdmin): class EmptyModelAdmin(admin.ModelAdmin): - def queryset(self, request): - return super(EmptyModelAdmin, self).queryset(request).filter(pk__gt=1) + def get_queryset(self, request): + return super(EmptyModelAdmin, self).get_queryset(request).filter(pk__gt=1) class OldSubscriberAdmin(admin.ModelAdmin): @@ -427,8 +427,8 @@ class PostAdmin(admin.ModelAdmin): class CustomChangeList(ChangeList): - def get_query_set(self, request): - return self.root_query_set.filter(pk=9999) # Does not exist + def get_queryset(self, request): + return self.root_queryset.filter(pk=9999) # Does not exist class GadgetAdmin(admin.ModelAdmin): @@ -452,52 +452,52 @@ class FoodDeliveryAdmin(admin.ModelAdmin): class CoverLetterAdmin(admin.ModelAdmin): """ - A ModelAdmin with a custom queryset() method that uses defer(), to test + A ModelAdmin with a custom get_queryset() method that uses defer(), to test verbose_name display in messages shown after adding/editing CoverLetter instances. Note that the CoverLetter model defines a __unicode__ method. For testing fix for ticket #14529. """ - def queryset(self, request): - return super(CoverLetterAdmin, self).queryset(request).defer('date_written') + def get_queryset(self, request): + return super(CoverLetterAdmin, self).get_queryset(request).defer('date_written') class PaperAdmin(admin.ModelAdmin): """ - A ModelAdmin with a custom queryset() method that uses only(), to test + A ModelAdmin with a custom get_queryset() method that uses only(), to test verbose_name display in messages shown after adding/editing Paper instances. For testing fix for ticket #14529. """ - def queryset(self, request): - return super(PaperAdmin, self).queryset(request).only('title') + def get_queryset(self, request): + return super(PaperAdmin, self).get_queryset(request).only('title') class ShortMessageAdmin(admin.ModelAdmin): """ - A ModelAdmin with a custom queryset() method that uses defer(), to test + A ModelAdmin with a custom get_queryset() method that uses defer(), to test verbose_name display in messages shown after adding/editing ShortMessage instances. For testing fix for ticket #14529. """ - def queryset(self, request): - return super(ShortMessageAdmin, self).queryset(request).defer('timestamp') + def get_queryset(self, request): + return super(ShortMessageAdmin, self).get_queryset(request).defer('timestamp') class TelegramAdmin(admin.ModelAdmin): """ - A ModelAdmin with a custom queryset() method that uses only(), to test + A ModelAdmin with a custom get_queryset() method that uses only(), to test verbose_name display in messages shown after adding/editing Telegram instances. Note that the Telegram model defines a __unicode__ method. For testing fix for ticket #14529. """ - def queryset(self, request): - return super(TelegramAdmin, self).queryset(request).only('title') + def get_queryset(self, request): + return super(TelegramAdmin, self).get_queryset(request).only('title') class StoryForm(forms.ModelForm): diff --git a/tests/admin_views/customadmin.py b/tests/admin_views/customadmin.py index d69d690af0..c204b81edd 100644 --- a/tests/admin_views/customadmin.py +++ b/tests/admin_views/customadmin.py @@ -35,8 +35,8 @@ class Admin2(admin.AdminSite): class UserLimitedAdmin(UserAdmin): # used for testing password change on a user not in queryset - def queryset(self, request): - qs = super(UserLimitedAdmin, self).queryset(request) + def get_queryset(self, request): + qs = super(UserLimitedAdmin, self).get_queryset(request) return qs.filter(is_superuser=False) diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index ff2eb95745..53dc74fa88 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -291,7 +291,7 @@ class AdminViewBasicTest(TestCase): """ If no ordering is defined in `ModelAdmin.ordering` or in the query string, then the underlying order of the queryset should not be - changed, even if it is defined in `Modeladmin.queryset()`. + changed, even if it is defined in `Modeladmin.get_queryset()`. Refs #11868, #7309. """ p1 = Person.objects.create(name="Amy", gender=1, alive=True, age=80) @@ -440,7 +440,7 @@ class AdminViewBasicTest(TestCase): self.urlbit, query_string)) self.assertEqual(filtered_response.status_code, 200) # ensure changelist contains only valid objects - for obj in filtered_response.context['cl'].query_set.all(): + for obj in filtered_response.context['cl'].queryset.all(): self.assertTrue(params['test'](obj, value)) def testIncorrectLookupParameters(self): @@ -2583,7 +2583,7 @@ class AdminCustomQuerysetTest(TestCase): self.assertEqual(response.status_code, 404) def test_add_model_modeladmin_defer_qs(self): - # Test for #14529. defer() is used in ModelAdmin.queryset() + # Test for #14529. defer() is used in ModelAdmin.get_queryset() # model has __unicode__ method self.assertEqual(CoverLetter.objects.count(), 0) @@ -2622,7 +2622,7 @@ class AdminCustomQuerysetTest(TestCase): ) def test_add_model_modeladmin_only_qs(self): - # Test for #14529. only() is used in ModelAdmin.queryset() + # Test for #14529. only() is used in ModelAdmin.get_queryset() # model has __unicode__ method self.assertEqual(Telegram.objects.count(), 0) @@ -2661,7 +2661,7 @@ class AdminCustomQuerysetTest(TestCase): ) def test_edit_model_modeladmin_defer_qs(self): - # Test for #14529. defer() is used in ModelAdmin.queryset() + # Test for #14529. defer() is used in ModelAdmin.get_queryset() # model has __unicode__ method cl = CoverLetter.objects.create(author="John Doe") @@ -2708,7 +2708,7 @@ class AdminCustomQuerysetTest(TestCase): ) def test_edit_model_modeladmin_only_qs(self): - # Test for #14529. only() is used in ModelAdmin.queryset() + # Test for #14529. only() is used in ModelAdmin.get_queryset() # model has __unicode__ method t = Telegram.objects.create(title="Frist Telegram") diff --git a/tests/admin_widgets/models.py b/tests/admin_widgets/models.py index 2977b86f3e..ae19d58cc4 100644 --- a/tests/admin_widgets/models.py +++ b/tests/admin_widgets/models.py @@ -37,8 +37,8 @@ class Album(models.Model): return self.name class HiddenInventoryManager(models.Manager): - def get_query_set(self): - return super(HiddenInventoryManager, self).get_query_set().filter(hidden=False) + def get_queryset(self): + return super(HiddenInventoryManager, self).get_queryset().filter(hidden=False) @python_2_unicode_compatible class Inventory(models.Model): diff --git a/tests/custom_managers/models.py b/tests/custom_managers/models.py index de7c1772ed..2f5e62fc7a 100644 --- a/tests/custom_managers/models.py +++ b/tests/custom_managers/models.py @@ -30,11 +30,11 @@ class Person(models.Model): def __str__(self): return "%s %s" % (self.first_name, self.last_name) -# An example of a custom manager that sets get_query_set(). +# An example of a custom manager that sets get_queryset(). class PublishedBookManager(models.Manager): - def get_query_set(self): - return super(PublishedBookManager, self).get_query_set().filter(is_published=True) + def get_queryset(self): + return super(PublishedBookManager, self).get_queryset().filter(is_published=True) @python_2_unicode_compatible class Book(models.Model): @@ -50,8 +50,8 @@ class Book(models.Model): # An example of providing multiple custom managers. class FastCarManager(models.Manager): - def get_query_set(self): - return super(FastCarManager, self).get_query_set().filter(top_speed__gt=150) + def get_queryset(self): + return super(FastCarManager, self).get_queryset().filter(top_speed__gt=150) @python_2_unicode_compatible class Car(models.Model): diff --git a/tests/custom_managers_regress/models.py b/tests/custom_managers_regress/models.py index 71073f0fe7..95cf6e8ca1 100644 --- a/tests/custom_managers_regress/models.py +++ b/tests/custom_managers_regress/models.py @@ -10,8 +10,8 @@ class RestrictedManager(models.Manager): """ A manager that filters out non-public instances. """ - def get_query_set(self): - return super(RestrictedManager, self).get_query_set().filter(is_public=True) + def get_queryset(self): + return super(RestrictedManager, self).get_queryset().filter(is_public=True) @python_2_unicode_compatible class RelatedModel(models.Model): diff --git a/tests/deprecation/__init__.py b/tests/deprecation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/deprecation/models.py b/tests/deprecation/models.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/deprecation/tests.py b/tests/deprecation/tests.py new file mode 100644 index 0000000000..df752b3149 --- /dev/null +++ b/tests/deprecation/tests.py @@ -0,0 +1,158 @@ +from __future__ import unicode_literals +import warnings + +from django.test.testcases import SimpleTestCase +from django.utils import six +from django.utils.deprecation import RenameMethodsBase + + +class RenameManagerMethods(RenameMethodsBase): + renamed_methods = ( + ('old', 'new', PendingDeprecationWarning), + ) + + +class RenameMethodsTests(SimpleTestCase): + """ + Tests the `RenameMethodsBase` type introduced to rename `get_query_set` + to `get_queryset` across the code base following #15363. + """ + + def test_class_definition_warnings(self): + """ + Ensure a warning is raised upon class definition to suggest renaming + the faulty method. + """ + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter('always') + class Manager(six.with_metaclass(RenameManagerMethods)): + def old(self): + pass + self.assertEqual(len(recorded), 1) + msg = str(recorded[0].message) + self.assertEqual(msg, + '`Manager.old` method should be renamed `new`.') + + def test_get_new_defined(self): + """ + Ensure `old` complains and not `new` when only `new` is defined. + """ + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter('ignore') + class Manager(six.with_metaclass(RenameManagerMethods)): + def new(self): + pass + warnings.simplefilter('always') + manager = Manager() + manager.new() + self.assertEqual(len(recorded), 0) + manager.old() + self.assertEqual(len(recorded), 1) + msg = str(recorded.pop().message) + self.assertEqual(msg, + '`Manager.old` is deprecated, use `new` instead.') + + def test_get_old_defined(self): + """ + Ensure `old` complains when only `old` is defined. + """ + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter('ignore') + class Manager(six.with_metaclass(RenameManagerMethods)): + def old(self): + pass + warnings.simplefilter('always') + manager = Manager() + manager.new() + self.assertEqual(len(recorded), 0) + manager.old() + self.assertEqual(len(recorded), 1) + msg = str(recorded.pop().message) + self.assertEqual(msg, + '`Manager.old` is deprecated, use `new` instead.') + + def test_deprecated_subclass_renamed(self): + """ + Ensure the correct warnings are raised when a class that didn't rename + `old` subclass one that did. + """ + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter('ignore') + class Renamed(six.with_metaclass(RenameManagerMethods)): + def new(self): + pass + class Deprecated(Renamed): + def old(self): + super(Deprecated, self).old() + warnings.simplefilter('always') + deprecated = Deprecated() + deprecated.new() + self.assertEqual(len(recorded), 1) + msg = str(recorded.pop().message) + self.assertEqual(msg, + '`Renamed.old` is deprecated, use `new` instead.') + recorded[:] = [] + deprecated.old() + self.assertEqual(len(recorded), 2) + msgs = [str(warning.message) for warning in recorded] + self.assertEqual(msgs, [ + '`Deprecated.old` is deprecated, use `new` instead.', + '`Renamed.old` is deprecated, use `new` instead.', + ]) + + def test_renamed_subclass_deprecated(self): + """ + Ensure the correct warnings are raised when a class that renamed + `old` subclass one that didn't. + """ + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter('ignore') + class Deprecated(six.with_metaclass(RenameManagerMethods)): + def old(self): + pass + class Renamed(Deprecated): + def new(self): + super(Renamed, self).new() + warnings.simplefilter('always') + renamed = Renamed() + renamed.new() + self.assertEqual(len(recorded), 0) + renamed.old() + self.assertEqual(len(recorded), 1) + msg = str(recorded.pop().message) + self.assertEqual(msg, + '`Renamed.old` is deprecated, use `new` instead.') + + def test_deprecated_subclass_renamed_and_mixins(self): + """ + Ensure the correct warnings are raised when a subclass inherit from a + class that renamed `old` and mixins that may or may not have renamed + `new`. + """ + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter('ignore') + class Renamed(six.with_metaclass(RenameManagerMethods)): + def new(self): + pass + class RenamedMixin(object): + def new(self): + super(RenamedMixin, self).new() + class DeprecatedMixin(object): + def old(self): + super(DeprecatedMixin, self).old() + class Deprecated(DeprecatedMixin, RenamedMixin, Renamed): + pass + warnings.simplefilter('always') + deprecated = Deprecated() + deprecated.new() + self.assertEqual(len(recorded), 1) + msg = str(recorded.pop().message) + self.assertEqual(msg, + '`RenamedMixin.old` is deprecated, use `new` instead.') + deprecated.old() + self.assertEqual(len(recorded), 2) + msgs = [str(warning.message) for warning in recorded] + self.assertEqual(msgs, [ + '`DeprecatedMixin.old` is deprecated, use `new` instead.', + '`RenamedMixin.old` is deprecated, use `new` instead.', + ]) diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py index 8bd3501926..976716fdc9 100644 --- a/tests/fixtures/models.py +++ b/tests/fixtures/models.py @@ -78,8 +78,8 @@ class Person(models.Model): return (self.name,) class SpyManager(PersonManager): - def get_query_set(self): - return super(SpyManager, self).get_query_set().filter(cover_blown=False) + def get_queryset(self): + return super(SpyManager, self).get_queryset().filter(cover_blown=False) class Spy(Person): objects = SpyManager() diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py index 18d7623971..34dc8d3a7d 100644 --- a/tests/generic_relations/models.py +++ b/tests/generic_relations/models.py @@ -88,8 +88,8 @@ class Mineral(models.Model): return self.name class GeckoManager(models.Manager): - def get_query_set(self): - return super(GeckoManager, self).get_query_set().filter(has_tail=True) + def get_queryset(self): + return super(GeckoManager, self).get_queryset().filter(has_tail=True) class Gecko(models.Model): has_tail = models.BooleanField() diff --git a/tests/get_object_or_404/models.py b/tests/get_object_or_404/models.py index bda060569e..bb9aa60383 100644 --- a/tests/get_object_or_404/models.py +++ b/tests/get_object_or_404/models.py @@ -22,8 +22,8 @@ class Author(models.Model): return self.name class ArticleManager(models.Manager): - def get_query_set(self): - return super(ArticleManager, self).get_query_set().filter(authors__name__icontains='sir') + def get_queryset(self): + return super(ArticleManager, self).get_queryset().filter(authors__name__icontains='sir') @python_2_unicode_compatible class Article(models.Model): diff --git a/tests/managers_regress/models.py b/tests/managers_regress/models.py index d72970d86e..d8dd22ec9a 100644 --- a/tests/managers_regress/models.py +++ b/tests/managers_regress/models.py @@ -7,18 +7,18 @@ from django.utils.encoding import python_2_unicode_compatible class OnlyFred(models.Manager): - def get_query_set(self): - return super(OnlyFred, self).get_query_set().filter(name='fred') + def get_queryset(self): + return super(OnlyFred, self).get_queryset().filter(name='fred') class OnlyBarney(models.Manager): - def get_query_set(self): - return super(OnlyBarney, self).get_query_set().filter(name='barney') + def get_queryset(self): + return super(OnlyBarney, self).get_queryset().filter(name='barney') class Value42(models.Manager): - def get_query_set(self): - return super(Value42, self).get_query_set().filter(value=42) + def get_queryset(self): + return super(Value42, self).get_queryset().filter(value=42) class AbstractBase1(models.Model): diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index b0a181218b..e5450ab8ff 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -69,7 +69,7 @@ class ModelAdminTests(TestCase): # If we specify the fields argument, fieldsets_add and fielsets_change should # just stick the fields into a formsets structure and return it. class BandAdmin(ModelAdmin): - fields = ['name'] + fields = ['name'] ma = BandAdmin(Band, self.site) @@ -1074,7 +1074,7 @@ class ValidationTests(unittest.TestCase): return 'awesomeness' def get_choices(self, request): return (('bit', 'A bit awesome'), ('very', 'Very awesome'), ) - def get_query_set(self, cl, qs): + def get_queryset(self, cl, qs): return qs class ValidationTestModelAdmin(ModelAdmin): diff --git a/tests/prefetch_related/models.py b/tests/prefetch_related/models.py index e58997d200..81c569844f 100644 --- a/tests/prefetch_related/models.py +++ b/tests/prefetch_related/models.py @@ -87,8 +87,8 @@ class Qualification(models.Model): class TeacherManager(models.Manager): - def get_query_set(self): - return super(TeacherManager, self).get_query_set().prefetch_related('qualifications') + def get_queryset(self): + return super(TeacherManager, self).get_queryset().prefetch_related('qualifications') @python_2_unicode_compatible diff --git a/tests/proxy_models/models.py b/tests/proxy_models/models.py index 6c962aadc8..ffb36657e1 100644 --- a/tests/proxy_models/models.py +++ b/tests/proxy_models/models.py @@ -10,12 +10,12 @@ from django.utils.encoding import python_2_unicode_compatible # A couple of managers for testing managing overriding in proxy model cases. class PersonManager(models.Manager): - def get_query_set(self): - return super(PersonManager, self).get_query_set().exclude(name="fred") + def get_queryset(self): + return super(PersonManager, self).get_queryset().exclude(name="fred") class SubManager(models.Manager): - def get_query_set(self): - return super(SubManager, self).get_query_set().exclude(name="wilma") + def get_queryset(self): + return super(SubManager, self).get_queryset().exclude(name="wilma") @python_2_unicode_compatible class Person(models.Model): diff --git a/tests/queries/models.py b/tests/queries/models.py index c8598371a6..f7f643d585 100644 --- a/tests/queries/models.py +++ b/tests/queries/models.py @@ -176,8 +176,8 @@ class LoopZ(models.Model): # A model and custom default manager combination. class CustomManager(models.Manager): - def get_query_set(self): - qs = super(CustomManager, self).get_query_set() + def get_queryset(self): + qs = super(CustomManager, self).get_queryset() return qs.filter(public=True, tag__name='t1') @python_2_unicode_compatible @@ -197,8 +197,8 @@ class Detail(models.Model): data = models.CharField(max_length=10) class MemberManager(models.Manager): - def get_query_set(self): - return super(MemberManager, self).get_query_set().select_related("details") + def get_queryset(self): + return super(MemberManager, self).get_queryset().select_related("details") class Member(models.Model): name = models.CharField(max_length=10) diff --git a/tests/reverse_single_related/models.py b/tests/reverse_single_related/models.py index 898be8411b..30ba345120 100644 --- a/tests/reverse_single_related/models.py +++ b/tests/reverse_single_related/models.py @@ -2,8 +2,8 @@ from django.db import models class SourceManager(models.Manager): - def get_query_set(self): - return super(SourceManager, self).get_query_set().filter(is_public=True) + def get_queryset(self): + return super(SourceManager, self).get_queryset().filter(is_public=True) class Source(models.Model): is_public = models.BooleanField() -- cgit v1.3 From 7aacde84f2b499d9c35741cbfccb621af6b48903 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 2 Mar 2013 20:25:25 +0100 Subject: Made transaction.managed a no-op and deprecated it. enter_transaction_management() was nearly always followed by managed(). In three places it wasn't, but they will all be refactored eventually. The "forced" keyword argument avoids introducing behavior changes until then. This is mostly backwards-compatible, except, of course, for managed itself. There's a minor difference in _enter_transaction_management: the top self.transaction_state now contains the new 'managed' state rather than the previous one. Django doesn't access self.transaction_state in _enter_transaction_management. --- django/core/management/commands/loaddata.py | 1 - django/db/backends/__init__.py | 29 +++++++---------------------- django/db/models/deletion.py | 2 +- django/db/models/query.py | 4 ++-- django/db/transaction.py | 18 ++++++------------ django/middleware/transaction.py | 1 - django/test/testcases.py | 4 ---- docs/internals/deprecation.txt | 6 ++++-- tests/delete_regress/tests.py | 4 +--- tests/middleware/tests.py | 6 +----- tests/requests/tests.py | 2 -- tests/select_for_update/tests.py | 3 --- tests/serializers/tests.py | 1 - tests/transactions_regress/tests.py | 5 ----- 14 files changed, 22 insertions(+), 64 deletions(-) (limited to 'docs/internals') diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index ed47b8fbf1..77b9a44a43 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -75,7 +75,6 @@ class Command(BaseCommand): if commit: transaction.commit_unless_managed(using=self.using) transaction.enter_transaction_management(using=self.using) - transaction.managed(True, using=self.using) class SingleZipReader(zipfile.ZipFile): def __init__(self, *args, **kwargs): diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index fe26c98baf..f11ee35260 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -234,7 +234,7 @@ class BaseDatabaseWrapper(object): ##### Generic transaction management methods ##### - def enter_transaction_management(self, managed=True): + def enter_transaction_management(self, managed=True, forced=False): """ Enters transaction management for a running thread. It must be balanced with the appropriate leave_transaction_management call, since the actual state is @@ -243,12 +243,14 @@ class BaseDatabaseWrapper(object): The state and dirty flag are carried over from the surrounding block or from the settings, if there is no surrounding block (dirty is always false when no current block is running). + + If you switch off transaction management and there is a pending + commit/rollback, the data will be commited, unless "forced" is True. """ - if self.transaction_state: - self.transaction_state.append(self.transaction_state[-1]) - else: - self.transaction_state.append(settings.TRANSACTIONS_MANAGED) + self.transaction_state.append(managed) self._enter_transaction_management(managed) + if not managed and self.is_dirty() and not forced: + self.commit() def leave_transaction_management(self): """ @@ -314,22 +316,6 @@ class BaseDatabaseWrapper(object): return self.transaction_state[-1] return settings.TRANSACTIONS_MANAGED - def managed(self, flag=True): - """ - Puts the transaction manager into a manual state: managed transactions have - to be committed explicitly by the user. If you switch off transaction - management and there is a pending commit/rollback, the data will be - commited. - """ - top = self.transaction_state - if top: - top[-1] = flag - if not flag and self.is_dirty(): - self.commit() - else: - raise TransactionManagementError("This code isn't under transaction " - "management") - def commit_unless_managed(self): """ Commits changes if the system is not in managed transaction mode. @@ -574,7 +560,6 @@ class BaseDatabaseFeatures(object): # otherwise autocommit will cause the confimation to # fail. self.connection.enter_transaction_management() - self.connection.managed(True) cursor = self.connection.cursor() cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)') self.connection.commit() diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 81f74923c2..93ef0006cb 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -54,7 +54,7 @@ def force_managed(func): @wraps(func) def decorated(self, *args, **kwargs): if not transaction.is_managed(using=self.using): - transaction.enter_transaction_management(using=self.using) + transaction.enter_transaction_management(using=self.using, forced=True) forced_managed = True else: forced_managed = False diff --git a/django/db/models/query.py b/django/db/models/query.py index 30be30ca43..b41007ee4f 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -443,7 +443,7 @@ class QuerySet(object): connection = connections[self.db] fields = self.model._meta.local_fields if not transaction.is_managed(using=self.db): - transaction.enter_transaction_management(using=self.db) + transaction.enter_transaction_management(using=self.db, forced=True) forced_managed = True else: forced_managed = False @@ -582,7 +582,7 @@ class QuerySet(object): query = self.query.clone(sql.UpdateQuery) query.add_update_values(kwargs) if not transaction.is_managed(using=self.db): - transaction.enter_transaction_management(using=self.db) + transaction.enter_transaction_management(using=self.db, forced=True) forced_managed = True else: forced_managed = False diff --git a/django/db/transaction.py b/django/db/transaction.py index 809f14f628..09ce2abbd2 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -12,6 +12,8 @@ Managed transactions don't do those commits, but will need some kind of manual or implicit commits or rollbacks. """ +import warnings + from functools import wraps from django.db import connections, DEFAULT_DB_ALIAS @@ -49,7 +51,7 @@ def abort(using=None): """ get_connection(using).abort() -def enter_transaction_management(managed=True, using=None): +def enter_transaction_management(managed=True, using=None, forced=False): """ Enters transaction management for a running thread. It must be balanced with the appropriate leave_transaction_management call, since the actual state is @@ -59,7 +61,7 @@ def enter_transaction_management(managed=True, using=None): from the settings, if there is no surrounding block (dirty is always false when no current block is running). """ - get_connection(using).enter_transaction_management(managed) + get_connection(using).enter_transaction_management(managed, forced) def leave_transaction_management(using=None): """ @@ -105,13 +107,8 @@ def is_managed(using=None): return get_connection(using).is_managed() def managed(flag=True, using=None): - """ - Puts the transaction manager into a manual state: managed transactions have - to be committed explicitly by the user. If you switch off transaction - management and there is a pending commit/rollback, the data will be - commited. - """ - get_connection(using).managed(flag) + warnings.warn("'managed' no longer serves a purpose.", + PendingDeprecationWarning, stacklevel=2) def commit_unless_managed(using=None): """ @@ -224,7 +221,6 @@ def autocommit(using=None): """ def entering(using): enter_transaction_management(managed=False, using=using) - managed(False, using=using) def exiting(exc_value, using): leave_transaction_management(using=using) @@ -240,7 +236,6 @@ def commit_on_success(using=None): """ def entering(using): enter_transaction_management(using=using) - managed(True, using=using) def exiting(exc_value, using): try: @@ -268,7 +263,6 @@ def commit_manually(using=None): """ def entering(using): enter_transaction_management(using=using) - managed(True, using=using) def exiting(exc_value, using): leave_transaction_management(using=using) diff --git a/django/middleware/transaction.py b/django/middleware/transaction.py index 4440f377a7..b5a07a02b7 100644 --- a/django/middleware/transaction.py +++ b/django/middleware/transaction.py @@ -10,7 +10,6 @@ class TransactionMiddleware(object): def process_request(self, request): """Enters transaction management""" transaction.enter_transaction_management() - transaction.managed(True) def process_exception(self, request, exception): """Rolls back the database and leaves transaction management""" diff --git a/django/test/testcases.py b/django/test/testcases.py index 44ddb624d6..7f6b1a49ba 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -67,7 +67,6 @@ real_commit = transaction.commit real_rollback = transaction.rollback real_enter_transaction_management = transaction.enter_transaction_management real_leave_transaction_management = transaction.leave_transaction_management -real_managed = transaction.managed real_abort = transaction.abort def nop(*args, **kwargs): @@ -78,7 +77,6 @@ def disable_transaction_methods(): transaction.rollback = nop transaction.enter_transaction_management = nop transaction.leave_transaction_management = nop - transaction.managed = nop transaction.abort = nop def restore_transaction_methods(): @@ -86,7 +84,6 @@ def restore_transaction_methods(): transaction.rollback = real_rollback transaction.enter_transaction_management = real_enter_transaction_management transaction.leave_transaction_management = real_leave_transaction_management - transaction.managed = real_managed transaction.abort = real_abort @@ -833,7 +830,6 @@ class TestCase(TransactionTestCase): for db_name in self._databases_names(): transaction.enter_transaction_management(using=db_name) - transaction.managed(True, using=db_name) disable_transaction_methods() from django.contrib.sites.models import Site diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index b5173af298..296f908a5b 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -339,8 +339,6 @@ these changes. * ``Model._meta.module_name`` was renamed to ``model_name``. -* The private API ``django.db.close_connection`` will be removed. - * Remove the backward compatible shims introduced to rename ``get_query_set`` and similar queryset methods. This affects the following classes: ``BaseModelAdmin``, ``ChangeList``, ``BaseCommentNode``, @@ -350,6 +348,10 @@ these changes. * Remove the backward compatible shims introduced to rename the attributes ``ChangeList.root_query_set`` and ``ChangeList.query_set``. +* The private API ``django.db.close_connection`` will be removed. + +* The private API ``django.transaction.managed`` will be removed. + 2.0 --- diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py index 9fcc19ba71..e88c95e229 100644 --- a/tests/delete_regress/tests.py +++ b/tests/delete_regress/tests.py @@ -22,9 +22,7 @@ class DeleteLockingTest(TransactionTestCase): self.conn2 = new_connections[DEFAULT_DB_ALIAS] # Put both DB connections into managed transaction mode transaction.enter_transaction_management() - transaction.managed(True) self.conn2.enter_transaction_management() - self.conn2.managed(True) def tearDown(self): # Close down the second connection. @@ -335,7 +333,7 @@ class Ticket19102Tests(TestCase): ).select_related('orgunit').delete() self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) - + @skipUnlessDBFeature("update_can_self_select") def test_ticket_19102_defer(self): with self.assertNumQueries(1): diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index 4e5fd8ea6b..122371c02c 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -692,7 +692,6 @@ class TransactionMiddlewareTest(TransactionTestCase): def test_managed_response(self): transaction.enter_transaction_management() - transaction.managed(True) Band.objects.create(name='The Beatles') self.assertTrue(transaction.is_dirty()) TransactionMiddleware().process_response(self.request, self.response) @@ -700,8 +699,7 @@ class TransactionMiddlewareTest(TransactionTestCase): self.assertEqual(Band.objects.count(), 1) def test_unmanaged_response(self): - transaction.enter_transaction_management() - transaction.managed(False) + transaction.enter_transaction_management(False) self.assertEqual(Band.objects.count(), 0) TransactionMiddleware().process_response(self.request, self.response) self.assertFalse(transaction.is_managed()) @@ -711,7 +709,6 @@ class TransactionMiddlewareTest(TransactionTestCase): def test_exception(self): transaction.enter_transaction_management() - transaction.managed(True) Band.objects.create(name='The Beatles') self.assertTrue(transaction.is_dirty()) TransactionMiddleware().process_exception(self.request, None) @@ -726,7 +723,6 @@ class TransactionMiddlewareTest(TransactionTestCase): raise IntegrityError() connections[DEFAULT_DB_ALIAS].commit = raise_exception transaction.enter_transaction_management() - transaction.managed(True) Band.objects.create(name='The Beatles') self.assertTrue(transaction.is_dirty()) with self.assertRaises(IntegrityError): diff --git a/tests/requests/tests.py b/tests/requests/tests.py index 4fdc17618b..2803d7995b 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -576,7 +576,6 @@ class DatabaseConnectionHandlingTests(TransactionTestCase): # Make sure there is an open connection connection.cursor() connection.enter_transaction_management() - connection.managed(True) signals.request_finished.send(sender=response._handler_class) self.assertEqual(len(connection.transaction_state), 0) @@ -585,7 +584,6 @@ class DatabaseConnectionHandlingTests(TransactionTestCase): connection.settings_dict['CONN_MAX_AGE'] = 0 connection.enter_transaction_management() - connection.managed(True) connection.set_dirty() # Test that the rollback doesn't succeed (for example network failure # could cause this). diff --git a/tests/select_for_update/tests.py b/tests/select_for_update/tests.py index b9716bd797..c2fa22705a 100644 --- a/tests/select_for_update/tests.py +++ b/tests/select_for_update/tests.py @@ -25,7 +25,6 @@ class SelectForUpdateTests(TransactionTestCase): def setUp(self): transaction.enter_transaction_management() - transaction.managed(True) self.person = Person.objects.create(name='Reinhardt') # We have to commit here so that code in run_select_for_update can @@ -37,7 +36,6 @@ class SelectForUpdateTests(TransactionTestCase): new_connections = ConnectionHandler(settings.DATABASES) self.new_connection = new_connections[DEFAULT_DB_ALIAS] self.new_connection.enter_transaction_management() - self.new_connection.managed(True) # We need to set settings.DEBUG to True so we can capture # the output SQL to examine. @@ -162,7 +160,6 @@ class SelectForUpdateTests(TransactionTestCase): # We need to enter transaction management again, as this is done on # per-thread basis transaction.enter_transaction_management() - transaction.managed(True) people = list( Person.objects.all().select_for_update(nowait=nowait) ) diff --git a/tests/serializers/tests.py b/tests/serializers/tests.py index 34d0f5f1b1..a96a1af748 100644 --- a/tests/serializers/tests.py +++ b/tests/serializers/tests.py @@ -268,7 +268,6 @@ class SerializersTransactionTestBase(object): # within a transaction in order to test forward reference # handling. transaction.enter_transaction_management() - transaction.managed(True) objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str) with connection.constraint_checks_disabled(): for obj in objs: diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index 6ba04892cd..0af7605339 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -223,7 +223,6 @@ class TestNewConnection(TransactionTestCase): def test_commit_unless_managed_in_managed(self): cursor = connection.cursor() connection.enter_transaction_management() - transaction.managed(True) cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") connection.commit_unless_managed() self.assertTrue(connection.is_dirty()) @@ -280,7 +279,6 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase): def test_transaction_management(self): transaction.enter_transaction_management() - transaction.managed(True) self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() @@ -288,7 +286,6 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase): def test_transaction_stacking(self): transaction.enter_transaction_management() - transaction.managed(True) self.assertEqual(connection.isolation_level, self._serializable) transaction.enter_transaction_management() @@ -302,13 +299,11 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase): def test_enter_autocommit(self): transaction.enter_transaction_management() - transaction.managed(True) self.assertEqual(connection.isolation_level, self._serializable) list(Mod.objects.all()) self.assertTrue(transaction.is_dirty()) # Enter autocommit mode again. transaction.enter_transaction_management(False) - transaction.managed(False) self.assertFalse(transaction.is_dirty()) self.assertEqual( connection.connection.get_transaction_status(), -- cgit v1.3 From f5156194945661d217523d6648dfb9b48707ec95 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 2 Mar 2013 13:47:46 +0100 Subject: Added an API to control database-level autocommit. --- django/db/backends/__init__.py | 14 ++++++++++++++ django/db/backends/creation.py | 6 +++++- django/db/backends/dummy/base.py | 1 + django/db/backends/mysql/base.py | 3 +++ django/db/backends/oracle/base.py | 3 +++ django/db/backends/oracle/creation.py | 3 --- django/db/backends/postgresql_psycopg2/base.py | 8 ++++++++ django/db/backends/postgresql_psycopg2/creation.py | 3 --- django/db/backends/sqlite3/base.py | 11 +++++++++++ django/db/backends/sqlite3/creation.py | 3 --- django/db/transaction.py | 12 ++++++++++++ django/test/testcases.py | 3 +++ docs/internals/deprecation.txt | 7 ++++--- docs/topics/db/transactions.txt | 17 +++++++++++++++++ 14 files changed, 81 insertions(+), 13 deletions(-) (limited to 'docs/internals') diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index f11ee35260..379416fad7 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -44,6 +44,7 @@ class BaseDatabaseWrapper(object): self.savepoint_state = 0 # Transaction management related attributes + self.autocommit = False self.transaction_state = [] # Tracks if the connection is believed to be in transaction. This is # set somewhat aggressively, as the DBAPI doesn't make it easy to @@ -232,6 +233,12 @@ class BaseDatabaseWrapper(object): """ pass + def _set_autocommit(self, autocommit): + """ + Backend-specific implementation to enable or disable autocommit. + """ + raise NotImplementedError + ##### Generic transaction management methods ##### def enter_transaction_management(self, managed=True, forced=False): @@ -274,6 +281,13 @@ class BaseDatabaseWrapper(object): raise TransactionManagementError( "Transaction managed block ended with pending COMMIT/ROLLBACK") + def set_autocommit(self, autocommit=True): + """ + Enable or disable autocommit. + """ + self._set_autocommit(autocommit) + self.autocommit = autocommit + def abort(self): """ Roll back any ongoing transaction and clean the transaction state diff --git a/django/db/backends/creation.py b/django/db/backends/creation.py index 70c24bc820..aa4fb82b12 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/creation.py @@ -1,6 +1,7 @@ import hashlib import sys import time +import warnings from django.conf import settings from django.db.utils import load_backend @@ -466,7 +467,10 @@ class BaseDatabaseCreation(object): anymore by Django code. Kept for compatibility with user code that might use it. """ - pass + warnings.warn( + "set_autocommit was moved from BaseDatabaseCreation to " + "BaseDatabaseWrapper.", PendingDeprecationWarning, stacklevel=2) + return self.connection.set_autocommit() def _prepare_for_test_db_ddl(self): """ diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index c59f037a27..a8c2d5bada 100644 --- a/django/db/backends/dummy/base.py +++ b/django/db/backends/dummy/base.py @@ -57,6 +57,7 @@ class DatabaseWrapper(BaseDatabaseWrapper): _savepoint_rollback = ignore _enter_transaction_management = complain _leave_transaction_management = ignore + _set_autocommit = complain set_dirty = complain set_clean = complain commit_unless_managed = complain diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 400fe6cdac..39fd3695b7 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -445,6 +445,9 @@ class DatabaseWrapper(BaseDatabaseWrapper): except Database.NotSupportedError: pass + def _set_autocommit(self, autocommit): + self.connection.autocommit(autocommit) + def disable_constraint_checking(self): """ Disables foreign key checks, primarily for use in adding rows with forward references. Always returns True, diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index 60ee1ba632..d895c1583a 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -612,6 +612,9 @@ class DatabaseWrapper(BaseDatabaseWrapper): def _savepoint_commit(self, sid): pass + def _set_autocommit(self, autocommit): + self.connection.autocommit = autocommit + def check_constraints(self, table_names=None): """ To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index aaca74e8d1..5485830bf5 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -273,6 +273,3 @@ class DatabaseCreation(BaseDatabaseCreation): settings_dict['NAME'], self._test_database_user(), ) - - def set_autocommit(self): - self.connection.connection.autocommit = True diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index f9af507311..a14844433e 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -201,6 +201,14 @@ class DatabaseWrapper(BaseDatabaseWrapper): self.isolation_level = level self.features.uses_savepoints = bool(level) + def _set_autocommit(self, autocommit): + if autocommit: + level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT + else: + level = self.settings_dict["OPTIONS"].get('isolation_level', + psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) + self._set_isolation_level(level) + def set_dirty(self): if ((self.transaction_state and self.transaction_state[-1]) or not self.features.uses_autocommit): diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py index b19926b440..e6400d79a1 100644 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ b/django/db/backends/postgresql_psycopg2/creation.py @@ -78,9 +78,6 @@ class DatabaseCreation(BaseDatabaseCreation): ' text_pattern_ops')) return output - def set_autocommit(self): - self._prepare_for_test_db_ddl() - def _prepare_for_test_db_ddl(self): """Rollback and close the active transaction.""" # Make sure there is an open connection. diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 416a6293f5..9a37dd17fe 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -355,6 +355,17 @@ class DatabaseWrapper(BaseDatabaseWrapper): if self.settings_dict['NAME'] != ":memory:": BaseDatabaseWrapper.close(self) + def _set_autocommit(self, autocommit): + if autocommit: + level = None + else: + # sqlite3's internal default is ''. It's different from None. + # See Modules/_sqlite/connection.c. + level = '' + # 'isolation_level' is a misleading API. + # SQLite always runs at the SERIALIZABLE isolation level. + self.connection.isolation_level = level + def check_constraints(self, table_names=None): """ Checks each table name in `table_names` for rows with invalid foreign key references. This method is diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index c90a697e35..a9fb273f7a 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -72,9 +72,6 @@ class DatabaseCreation(BaseDatabaseCreation): # Remove the SQLite database file os.remove(test_database_name) - def set_autocommit(self): - self.connection.connection.isolation_level = None - def test_db_signature(self): """ Returns a tuple that uniquely identifies a test database. diff --git a/django/db/transaction.py b/django/db/transaction.py index 09ce2abbd2..dd48e14bf4 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -39,6 +39,18 @@ def get_connection(using=None): using = DEFAULT_DB_ALIAS return connections[using] +def get_autocommit(using=None): + """ + Get the autocommit status of the connection. + """ + return get_connection(using).autocommit + +def set_autocommit(using=None, autocommit=True): + """ + Set the autocommit status of the connection. + """ + return get_connection(using).set_autocommit(autocommit) + def abort(using=None): """ Roll back any ongoing transactions and clean the transaction management diff --git a/django/test/testcases.py b/django/test/testcases.py index 7f6b1a49ba..4b9116e3bc 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -63,6 +63,7 @@ def to_list(value): value = [value] return value +real_set_autocommit = transaction.set_autocommit real_commit = transaction.commit real_rollback = transaction.rollback real_enter_transaction_management = transaction.enter_transaction_management @@ -73,6 +74,7 @@ def nop(*args, **kwargs): return def disable_transaction_methods(): + transaction.set_autocommit = nop transaction.commit = nop transaction.rollback = nop transaction.enter_transaction_management = nop @@ -80,6 +82,7 @@ def disable_transaction_methods(): transaction.abort = nop def restore_transaction_methods(): + transaction.set_autocommit = real_set_autocommit transaction.commit = real_commit transaction.rollback = real_rollback transaction.enter_transaction_management = real_enter_transaction_management diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 296f908a5b..74fbb563f0 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -348,9 +348,10 @@ these changes. * Remove the backward compatible shims introduced to rename the attributes ``ChangeList.root_query_set`` and ``ChangeList.query_set``. -* The private API ``django.db.close_connection`` will be removed. - -* The private API ``django.transaction.managed`` will be removed. +* The following private APIs will be removed: + - ``django.db.close_connection()`` + - ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()`` + - ``django.db.transaction.managed()`` 2.0 --- diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 11755ff5c5..e145edf149 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -208,6 +208,23 @@ This applies to all database operations, not just write operations. Even if your transaction only reads from the database, the transaction must be committed or rolled back before you complete a request. +.. _managing-autocommit: + +Managing autocommit +=================== + +.. versionadded:: 1.6 + +Django provides a straightforward API to manage the autocommit state of each +database connection, if you need to. + +.. function:: get_autocommit(using=None) + +.. function:: set_autocommit(using=None, autocommit=True) + +These functions take a ``using`` argument which should be the name of a +database. If it isn't provided, Django uses the ``"default"`` database. + .. _deactivate-transaction-management: How to globally deactivate transaction management -- cgit v1.3 From ba5138b1c0253fcf390b7509ad7b954117b3be88 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 4 Mar 2013 13:12:59 +0100 Subject: Deprecated transaction.commit/rollback_unless_managed. Since "unless managed" now means "if database-level autocommit", committing or rolling back doesn't have any effect. Restored transactional integrity in a few places that relied on automatically-started transactions with a transitory API. --- django/contrib/gis/utils/layermapping.py | 4 -- django/contrib/sessions/backends/db.py | 1 - django/core/cache/backends/db.py | 7 +- .../core/management/commands/createcachetable.py | 21 +++--- django/core/management/commands/flush.py | 9 ++- django/core/management/commands/loaddata.py | 1 - django/core/management/commands/syncdb.py | 77 ++++++++++---------- django/db/__init__.py | 11 --- django/db/backends/__init__.py | 21 ------ django/db/backends/dummy/base.py | 2 - django/db/models/base.py | 84 +++++++++++----------- django/db/models/deletion.py | 2 - django/db/models/query.py | 4 -- django/db/transaction.py | 27 ++++--- django/test/testcases.py | 19 +---- docs/internals/deprecation.txt | 2 + tests/transactions_regress/tests.py | 32 --------- 17 files changed, 116 insertions(+), 208 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/gis/utils/layermapping.py b/django/contrib/gis/utils/layermapping.py index e4ea44d0d2..51f70f5350 100644 --- a/django/contrib/gis/utils/layermapping.py +++ b/django/contrib/gis/utils/layermapping.py @@ -555,10 +555,6 @@ class LayerMapping(object): except SystemExit: raise except Exception as msg: - if self.transaction_mode == 'autocommit': - # Rolling back the transaction so that other model saves - # will work. - transaction.rollback_unless_managed() if strict: # Bailing out if the `strict` keyword is set. if not silent: diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py index 47e89b66e5..30da0b7a10 100644 --- a/django/contrib/sessions/backends/db.py +++ b/django/contrib/sessions/backends/db.py @@ -74,7 +74,6 @@ class SessionStore(SessionBase): @classmethod def clear_expired(cls): Session.objects.filter(expire_date__lt=timezone.now()).delete() - transaction.commit_unless_managed() # At bottom to avoid circular import diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py index bb91d8cb05..53d7f4d22a 100644 --- a/django/core/cache/backends/db.py +++ b/django/core/cache/backends/db.py @@ -10,7 +10,7 @@ except ImportError: from django.conf import settings from django.core.cache.backends.base import BaseCache -from django.db import connections, router, transaction, DatabaseError +from django.db import connections, router, DatabaseError from django.utils import timezone, six from django.utils.encoding import force_bytes @@ -70,7 +70,6 @@ class DatabaseCache(BaseDatabaseCache): cursor = connections[db].cursor() cursor.execute("DELETE FROM %s " "WHERE cache_key = %%s" % table, [key]) - transaction.commit_unless_managed(using=db) return default value = connections[db].ops.process_clob(row[1]) return pickle.loads(base64.b64decode(force_bytes(value))) @@ -124,10 +123,8 @@ class DatabaseCache(BaseDatabaseCache): [key, b64encoded, connections[db].ops.value_to_db_datetime(exp)]) except DatabaseError: # To be threadsafe, updates/inserts are allowed to fail silently - transaction.rollback_unless_managed(using=db) return False else: - transaction.commit_unless_managed(using=db) return True def delete(self, key, version=None): @@ -139,7 +136,6 @@ class DatabaseCache(BaseDatabaseCache): cursor = connections[db].cursor() cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % table, [key]) - transaction.commit_unless_managed(using=db) def has_key(self, key, version=None): key = self.make_key(key, version=version) @@ -184,7 +180,6 @@ class DatabaseCache(BaseDatabaseCache): table = connections[db].ops.quote_name(self._table) cursor = connections[db].cursor() cursor.execute('DELETE FROM %s' % table) - transaction.commit_unless_managed(using=db) # For backwards compatibility class CacheClass(DatabaseCache): diff --git a/django/core/management/commands/createcachetable.py b/django/core/management/commands/createcachetable.py index 411042ee76..94b6b09400 100644 --- a/django/core/management/commands/createcachetable.py +++ b/django/core/management/commands/createcachetable.py @@ -53,14 +53,13 @@ class Command(LabelCommand): for i, line in enumerate(table_output): full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) full_statement.append(');') - curs = connection.cursor() - try: - curs.execute("\n".join(full_statement)) - except DatabaseError as e: - transaction.rollback_unless_managed(using=db) - raise CommandError( - "Cache table '%s' could not be created.\nThe error was: %s." % - (tablename, force_text(e))) - for statement in index_output: - curs.execute(statement) - transaction.commit_unless_managed(using=db) + with transaction.commit_on_success_unless_managed(): + curs = connection.cursor() + try: + curs.execute("\n".join(full_statement)) + except DatabaseError as e: + raise CommandError( + "Cache table '%s' could not be created.\nThe error was: %s." % + (tablename, force_text(e))) + for statement in index_output: + curs.execute(statement) diff --git a/django/core/management/commands/flush.py b/django/core/management/commands/flush.py index 3bf1e9c672..9bd65e735c 100644 --- a/django/core/management/commands/flush.py +++ b/django/core/management/commands/flush.py @@ -57,18 +57,17 @@ Are you sure you want to do this? if confirm == 'yes': try: - cursor = connection.cursor() - for sql in sql_list: - cursor.execute(sql) + with transaction.commit_on_success_unless_managed(): + cursor = connection.cursor() + for sql in sql_list: + cursor.execute(sql) except Exception as e: - transaction.rollback_unless_managed(using=db) raise CommandError("""Database %s couldn't be flushed. Possible reasons: * The database isn't running or isn't configured correctly. * At least one of the expected database tables doesn't exist. * The SQL was invalid. Hint: Look at the output of 'django-admin.py sqlflush'. That's the SQL this command wasn't able to run. The full error: %s""" % (connection.settings_dict['NAME'], e)) - transaction.commit_unless_managed(using=db) # Emit the post sync signal. This allows individual # applications to respond as if the database had been diff --git a/django/core/management/commands/loaddata.py b/django/core/management/commands/loaddata.py index 77b9a44a43..674e6be7b0 100644 --- a/django/core/management/commands/loaddata.py +++ b/django/core/management/commands/loaddata.py @@ -73,7 +73,6 @@ class Command(BaseCommand): # Start transaction management. All fixtures are installed in a # single transaction to ensure that all references are resolved. if commit: - transaction.commit_unless_managed(using=self.using) transaction.enter_transaction_management(using=self.using) class SingleZipReader(zipfile.ZipFile): diff --git a/django/core/management/commands/syncdb.py b/django/core/management/commands/syncdb.py index 4ce2910fb5..e7e11a8c90 100644 --- a/django/core/management/commands/syncdb.py +++ b/django/core/management/commands/syncdb.py @@ -83,26 +83,25 @@ class Command(NoArgsCommand): # Create the tables for each model if verbosity >= 1: self.stdout.write("Creating tables ...\n") - for app_name, model_list in manifest.items(): - for model in model_list: - # Create the model's database table, if it doesn't already exist. - if verbosity >= 3: - self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name)) - sql, references = connection.creation.sql_create_model(model, self.style, seen_models) - seen_models.add(model) - created_models.add(model) - for refto, refs in references.items(): - pending_references.setdefault(refto, []).extend(refs) - if refto in seen_models: - sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references)) - sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references)) - if verbosity >= 1 and sql: - self.stdout.write("Creating table %s\n" % model._meta.db_table) - for statement in sql: - cursor.execute(statement) - tables.append(connection.introspection.table_name_converter(model._meta.db_table)) - - transaction.commit_unless_managed(using=db) + with transaction.commit_on_success_unless_managed(using=db): + for app_name, model_list in manifest.items(): + for model in model_list: + # Create the model's database table, if it doesn't already exist. + if verbosity >= 3: + self.stdout.write("Processing %s.%s model\n" % (app_name, model._meta.object_name)) + sql, references = connection.creation.sql_create_model(model, self.style, seen_models) + seen_models.add(model) + created_models.add(model) + for refto, refs in references.items(): + pending_references.setdefault(refto, []).extend(refs) + if refto in seen_models: + sql.extend(connection.creation.sql_for_pending_references(refto, self.style, pending_references)) + sql.extend(connection.creation.sql_for_pending_references(model, self.style, pending_references)) + if verbosity >= 1 and sql: + self.stdout.write("Creating table %s\n" % model._meta.db_table) + for statement in sql: + cursor.execute(statement) + tables.append(connection.introspection.table_name_converter(model._meta.db_table)) # Send the post_syncdb signal, so individual apps can do whatever they need # to do at this point. @@ -122,17 +121,16 @@ class Command(NoArgsCommand): if custom_sql: if verbosity >= 2: self.stdout.write("Installing custom SQL for %s.%s model\n" % (app_name, model._meta.object_name)) - try: - for sql in custom_sql: - cursor.execute(sql) - except Exception as e: - self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \ - (app_name, model._meta.object_name, e)) - if show_traceback: - traceback.print_exc() - transaction.rollback_unless_managed(using=db) - else: - transaction.commit_unless_managed(using=db) + with transaction.commit_on_success_unless_managed(using=db): + try: + for sql in custom_sql: + cursor.execute(sql) + except Exception as e: + self.stderr.write("Failed to install custom SQL for %s.%s model: %s\n" % \ + (app_name, model._meta.object_name, e)) + if show_traceback: + traceback.print_exc() + raise else: if verbosity >= 3: self.stdout.write("No custom SQL for %s.%s model\n" % (app_name, model._meta.object_name)) @@ -147,15 +145,14 @@ class Command(NoArgsCommand): if index_sql: if verbosity >= 2: self.stdout.write("Installing index for %s.%s model\n" % (app_name, model._meta.object_name)) - try: - for sql in index_sql: - cursor.execute(sql) - except Exception as e: - self.stderr.write("Failed to install index for %s.%s model: %s\n" % \ - (app_name, model._meta.object_name, e)) - transaction.rollback_unless_managed(using=db) - else: - transaction.commit_unless_managed(using=db) + with transaction.commit_on_success_unless_managed(using=db): + try: + for sql in index_sql: + cursor.execute(sql) + except Exception as e: + self.stderr.write("Failed to install index for %s.%s model: %s\n" % \ + (app_name, model._meta.object_name, e)) + raise # Load initial_data fixtures (unless that has been disabled) if load_initial_data: diff --git a/django/db/__init__.py b/django/db/__init__.py index 60fe8f6ce2..13ba68ba7e 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -77,14 +77,3 @@ def close_old_connections(**kwargs): conn.close_if_unusable_or_obsolete() signals.request_started.connect(close_old_connections) signals.request_finished.connect(close_old_connections) - -# Register an event that rolls back the connections -# when a Django request has an exception. -def _rollback_on_exception(**kwargs): - from django.db import transaction - for conn in connections: - try: - transaction.rollback_unless_managed(using=conn) - except DatabaseError: - pass -signals.got_request_exception.connect(_rollback_on_exception) diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 4031e8f668..848f6df2d6 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -339,27 +339,6 @@ class BaseDatabaseWrapper(object): return self.transaction_state[-1] return settings.TRANSACTIONS_MANAGED - def commit_unless_managed(self): - """ - Commits changes if the system is not in managed transaction mode. - """ - self.validate_thread_sharing() - if not self.is_managed(): - self.commit() - self.clean_savepoints() - else: - self.set_dirty() - - def rollback_unless_managed(self): - """ - Rolls back changes if the system is not in managed transaction mode. - """ - self.validate_thread_sharing() - if not self.is_managed(): - self.rollback() - else: - self.set_dirty() - ##### Foreign key constraints checks handling ##### @contextmanager diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index 02f0b6462d..9a220ffd8b 100644 --- a/django/db/backends/dummy/base.py +++ b/django/db/backends/dummy/base.py @@ -58,8 +58,6 @@ class DatabaseWrapper(BaseDatabaseWrapper): _set_autocommit = complain set_dirty = complain set_clean = complain - commit_unless_managed = complain - rollback_unless_managed = ignore def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) diff --git a/django/db/models/base.py b/django/db/models/base.py index 543cdfc165..ab0e42d461 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -609,48 +609,48 @@ class Model(six.with_metaclass(ModelBase)): if update_fields: non_pks = [f for f in non_pks if f.name in update_fields or f.attname in update_fields] - # First, try an UPDATE. If that doesn't update anything, do an INSERT. - pk_val = self._get_pk_val(meta) - pk_set = pk_val is not None - record_exists = True - manager = cls._base_manager - if pk_set: - # Determine if we should do an update (pk already exists, forced update, - # no force_insert) - if ((force_update or update_fields) or (not force_insert and - manager.using(using).filter(pk=pk_val).exists())): - if force_update or non_pks: - values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] - if values: - rows = manager.using(using).filter(pk=pk_val)._update(values) - if force_update and not rows: - raise DatabaseError("Forced update did not affect any rows.") - if update_fields and not rows: - raise DatabaseError("Save with update_fields did not affect any rows.") - else: - record_exists = False - if not pk_set or not record_exists: - if meta.order_with_respect_to: - # If this is a model with an order_with_respect_to - # autopopulate the _order field - field = meta.order_with_respect_to - order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count() - self._order = order_value - - fields = meta.local_fields - if not pk_set: - if force_update or update_fields: - raise ValueError("Cannot force an update in save() with no primary key.") - fields = [f for f in fields if not isinstance(f, AutoField)] + with transaction.commit_on_success_unless_managed(using=using): + # First, try an UPDATE. If that doesn't update anything, do an INSERT. + pk_val = self._get_pk_val(meta) + pk_set = pk_val is not None + record_exists = True + manager = cls._base_manager + if pk_set: + # Determine if we should do an update (pk already exists, forced update, + # no force_insert) + if ((force_update or update_fields) or (not force_insert and + manager.using(using).filter(pk=pk_val).exists())): + if force_update or non_pks: + values = [(f, None, (raw and getattr(self, f.attname) or f.pre_save(self, False))) for f in non_pks] + if values: + rows = manager.using(using).filter(pk=pk_val)._update(values) + if force_update and not rows: + raise DatabaseError("Forced update did not affect any rows.") + if update_fields and not rows: + raise DatabaseError("Save with update_fields did not affect any rows.") + else: + record_exists = False + if not pk_set or not record_exists: + if meta.order_with_respect_to: + # If this is a model with an order_with_respect_to + # autopopulate the _order field + field = meta.order_with_respect_to + order_value = manager.using(using).filter(**{field.name: getattr(self, field.attname)}).count() + self._order = order_value + + fields = meta.local_fields + if not pk_set: + if force_update or update_fields: + raise ValueError("Cannot force an update in save() with no primary key.") + fields = [f for f in fields if not isinstance(f, AutoField)] - record_exists = False + record_exists = False - update_pk = bool(meta.has_auto_field and not pk_set) - result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw) + update_pk = bool(meta.has_auto_field and not pk_set) + result = manager._insert([self], fields=fields, return_id=update_pk, using=using, raw=raw) - if update_pk: - setattr(self, meta.pk.attname, result) - transaction.commit_unless_managed(using=using) + if update_pk: + setattr(self, meta.pk.attname, result) # Store the database on which the object was saved self._state.db = using @@ -963,9 +963,9 @@ def method_set_order(ordered_obj, self, id_list, using=None): order_name = ordered_obj._meta.order_with_respect_to.name # FIXME: It would be nice if there was an "update many" version of update # for situations like this. - for i, j in enumerate(id_list): - ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i) - transaction.commit_unless_managed(using=using) + with transaction.commit_on_success_unless_managed(using=using): + for i, j in enumerate(id_list): + ordered_obj.objects.filter(**{'pk': j, order_name: rel_val}).update(_order=i) def method_get_order(ordered_obj, self): diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 93ef0006cb..26f63391d5 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -62,8 +62,6 @@ def force_managed(func): func(self, *args, **kwargs) if forced_managed: transaction.commit(using=self.using) - else: - transaction.commit_unless_managed(using=self.using) finally: if forced_managed: transaction.leave_transaction_management(using=self.using) diff --git a/django/db/models/query.py b/django/db/models/query.py index b41007ee4f..22f71c6aee 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -460,8 +460,6 @@ class QuerySet(object): self._batched_insert(objs_without_pk, fields, batch_size) if forced_managed: transaction.commit(using=self.db) - else: - transaction.commit_unless_managed(using=self.db) finally: if forced_managed: transaction.leave_transaction_management(using=self.db) @@ -590,8 +588,6 @@ class QuerySet(object): rows = query.get_compiler(self.db).execute_sql(None) if forced_managed: transaction.commit(using=self.db) - else: - transaction.commit_unless_managed(using=self.db) finally: if forced_managed: transaction.leave_transaction_management(using=self.db) diff --git a/django/db/transaction.py b/django/db/transaction.py index dd48e14bf4..a8e80c6c02 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -123,16 +123,12 @@ def managed(flag=True, using=None): PendingDeprecationWarning, stacklevel=2) def commit_unless_managed(using=None): - """ - Commits changes if the system is not in managed transaction mode. - """ - get_connection(using).commit_unless_managed() + warnings.warn("'commit_unless_managed' is now a no-op.", + PendingDeprecationWarning, stacklevel=2) def rollback_unless_managed(using=None): - """ - Rolls back changes if the system is not in managed transaction mode. - """ - get_connection(using).rollback_unless_managed() + warnings.warn("'rollback_unless_managed' is now a no-op.", + PendingDeprecationWarning, stacklevel=2) ############### # Public APIs # @@ -280,3 +276,18 @@ def commit_manually(using=None): leave_transaction_management(using=using) return _transaction_func(entering, exiting, using) + +def commit_on_success_unless_managed(using=None): + """ + Transitory API to preserve backwards-compatibility while refactoring. + """ + if is_managed(using): + def entering(using): + pass + + def exiting(exc_value, using): + set_dirty(using=using) + + return _transaction_func(entering, exiting, using) + else: + return commit_on_success(using) diff --git a/django/test/testcases.py b/django/test/testcases.py index 4b9116e3bc..55673dca25 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -157,14 +157,6 @@ class DocTestRunner(doctest.DocTestRunner): doctest.DocTestRunner.__init__(self, *args, **kwargs) self.optionflags = doctest.ELLIPSIS - def report_unexpected_exception(self, out, test, example, exc_info): - doctest.DocTestRunner.report_unexpected_exception(self, out, test, - example, exc_info) - # Rollback, in case of database errors. Otherwise they'd have - # side effects on other tests. - for conn in connections: - transaction.rollback_unless_managed(using=conn) - class _AssertNumQueriesContext(CaptureQueriesContext): def __init__(self, test_case, num, connection): @@ -490,14 +482,10 @@ class TransactionTestCase(SimpleTestCase): conn.ops.sequence_reset_by_name_sql(no_style(), conn.introspection.sequence_list()) if sql_list: - try: + with transaction.commit_on_success_unless_managed(using=db_name): cursor = conn.cursor() for sql in sql_list: cursor.execute(sql) - except Exception: - transaction.rollback_unless_managed(using=db_name) - raise - transaction.commit_unless_managed(using=db_name) def _fixture_setup(self): for db_name in self._databases_names(include_mirrors=False): @@ -537,11 +525,6 @@ class TransactionTestCase(SimpleTestCase): conn.close() def _fixture_teardown(self): - # Roll back any pending transactions in order to avoid a deadlock - # during flush when TEST_MIRROR is used (#18984). - for conn in connections.all(): - conn.rollback_unless_managed() - for db in self._databases_names(include_mirrors=False): call_command('flush', verbosity=0, interactive=False, database=db, skip_validation=True, reset_sequences=False) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 74fbb563f0..a81b16278f 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -352,6 +352,8 @@ these changes. - ``django.db.close_connection()`` - ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()`` - ``django.db.transaction.managed()`` + - ``django.db.transaction.commit_unless_managed()`` + - ``django.db.transaction.rollback_unless_managed()`` 2.0 --- diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index e6750cddcf..3d571fba2f 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -208,38 +208,6 @@ class TestNewConnection(TransactionTestCase): connection.leave_transaction_management() self.assertEqual(orig_dirty, connection._dirty) - # TODO: update this test to account for database-level autocommit. - @expectedFailure - def test_commit_unless_managed(self): - cursor = connection.cursor() - cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") - connection.commit_unless_managed() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 1) - self.assertTrue(connection.is_dirty()) - connection.commit_unless_managed() - self.assertFalse(connection.is_dirty()) - - # TODO: update this test to account for database-level autocommit. - @expectedFailure - def test_commit_unless_managed_in_managed(self): - cursor = connection.cursor() - connection.enter_transaction_management() - cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") - connection.commit_unless_managed() - self.assertTrue(connection.is_dirty()) - connection.rollback() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 0) - connection.commit() - connection.leave_transaction_management() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 0) - self.assertTrue(connection.is_dirty()) - connection.commit_unless_managed() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 0) - @skipUnless(connection.vendor == 'postgresql', "This test only valid for PostgreSQL") -- cgit v1.3 From 3bdc7a6a70bb030324fdebe9b1dce1fa5358f0c6 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 4 Mar 2013 15:24:01 +0100 Subject: Deprecated transaction.is_managed(). It's synchronized with the autocommit flag. --- django/db/backends/__init__.py | 30 ++++++++++++------------------ django/db/models/deletion.py | 2 +- django/db/models/query.py | 4 ++-- django/db/transaction.py | 12 +++++------- django/middleware/transaction.py | 2 +- docs/internals/deprecation.txt | 1 + tests/middleware/tests.py | 2 +- 7 files changed, 23 insertions(+), 30 deletions(-) (limited to 'docs/internals') diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 848f6df2d6..499bc32113 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -256,11 +256,12 @@ class BaseDatabaseWrapper(object): """ self.transaction_state.append(managed) - if managed and self.autocommit: - self.set_autocommit(False) - if not managed and self.is_dirty() and not forced: self.commit() + self.set_clean() + + if managed == self.autocommit: + self.set_autocommit(not managed) def leave_transaction_management(self): """ @@ -274,19 +275,20 @@ class BaseDatabaseWrapper(object): raise TransactionManagementError( "This code isn't under transaction management") - # That's the next state -- we already left the previous state behind. - managed = self.is_managed() + if self.transaction_state: + managed = self.transaction_state[-1] + else: + managed = settings.TRANSACTIONS_MANAGED if self._dirty: self.rollback() - if not managed and not self.autocommit: - self.set_autocommit(True) + if managed == self.autocommit: + self.set_autocommit(not managed) raise TransactionManagementError( "Transaction managed block ended with pending COMMIT/ROLLBACK") - if not managed and not self.autocommit: - self.set_autocommit(True) - + if managed == self.autocommit: + self.set_autocommit(not managed) def set_autocommit(self, autocommit=True): """ @@ -331,14 +333,6 @@ class BaseDatabaseWrapper(object): self._dirty = False self.clean_savepoints() - def is_managed(self): - """ - Checks whether the transaction manager is in manual or in auto state. - """ - if self.transaction_state: - return self.transaction_state[-1] - return settings.TRANSACTIONS_MANAGED - ##### Foreign key constraints checks handling ##### @contextmanager diff --git a/django/db/models/deletion.py b/django/db/models/deletion.py index 26f63391d5..27184c9350 100644 --- a/django/db/models/deletion.py +++ b/django/db/models/deletion.py @@ -53,7 +53,7 @@ def DO_NOTHING(collector, field, sub_objs, using): def force_managed(func): @wraps(func) def decorated(self, *args, **kwargs): - if not transaction.is_managed(using=self.using): + if transaction.get_autocommit(using=self.using): transaction.enter_transaction_management(using=self.using, forced=True) forced_managed = True else: diff --git a/django/db/models/query.py b/django/db/models/query.py index 22f71c6aee..834fe363b4 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -442,7 +442,7 @@ class QuerySet(object): self._for_write = True connection = connections[self.db] fields = self.model._meta.local_fields - if not transaction.is_managed(using=self.db): + if transaction.get_autocommit(using=self.db): transaction.enter_transaction_management(using=self.db, forced=True) forced_managed = True else: @@ -579,7 +579,7 @@ class QuerySet(object): self._for_write = True query = self.query.clone(sql.UpdateQuery) query.add_update_values(kwargs) - if not transaction.is_managed(using=self.db): + if transaction.get_autocommit(using=self.db): transaction.enter_transaction_management(using=self.db, forced=True) forced_managed = True else: diff --git a/django/db/transaction.py b/django/db/transaction.py index a8e80c6c02..49b67f4122 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -113,10 +113,8 @@ def clean_savepoints(using=None): get_connection(using).clean_savepoints() def is_managed(using=None): - """ - Checks whether the transaction manager is in manual or in auto state. - """ - return get_connection(using).is_managed() + warnings.warn("'is_managed' is deprecated.", + PendingDeprecationWarning, stacklevel=2) def managed(flag=True, using=None): warnings.warn("'managed' no longer serves a purpose.", @@ -281,7 +279,9 @@ def commit_on_success_unless_managed(using=None): """ Transitory API to preserve backwards-compatibility while refactoring. """ - if is_managed(using): + if get_autocommit(using): + return commit_on_success(using) + else: def entering(using): pass @@ -289,5 +289,3 @@ def commit_on_success_unless_managed(using=None): set_dirty(using=using) return _transaction_func(entering, exiting, using) - else: - return commit_on_success(using) diff --git a/django/middleware/transaction.py b/django/middleware/transaction.py index b5a07a02b7..35f765d99f 100644 --- a/django/middleware/transaction.py +++ b/django/middleware/transaction.py @@ -23,7 +23,7 @@ class TransactionMiddleware(object): def process_response(self, request, response): """Commits and leaves transaction management.""" - if transaction.is_managed(): + if not transaction.get_autocommit(): if transaction.is_dirty(): # Note: it is possible that the commit fails. If the reason is # closed connection or some similar reason, then there is diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index a81b16278f..1c8618713a 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -351,6 +351,7 @@ these changes. * The following private APIs will be removed: - ``django.db.close_connection()`` - ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()`` + - ``django.db.transaction.is_managed()`` - ``django.db.transaction.managed()`` - ``django.db.transaction.commit_unless_managed()`` - ``django.db.transaction.rollback_unless_managed()`` diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index 17751dd158..e704fce342 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -689,7 +689,7 @@ class TransactionMiddlewareTest(TransactionTestCase): def test_request(self): TransactionMiddleware().process_request(self.request) - self.assertTrue(transaction.is_managed()) + self.assertFalse(transaction.get_autocommit()) def test_managed_response(self): transaction.enter_transaction_management() -- cgit v1.3 From 7c46c8d5f27fe305507359588ca0635b6d87c59a Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 4 Mar 2013 23:26:31 +0100 Subject: Added some assertions to enforce the atomicity of atomic. --- django/db/__init__.py | 1 + django/db/backends/__init__.py | 15 ++ django/db/transaction.py | 18 +- docs/internals/deprecation.txt | 4 + docs/releases/1.3-alpha-1.txt | 6 +- docs/releases/1.3.txt | 6 +- docs/releases/1.6.txt | 17 +- docs/topics/db/transactions.txt | 439 +++++++++++++++------------------- tests/backends/tests.py | 15 +- tests/fixtures_model_package/tests.py | 11 +- tests/fixtures_regress/tests.py | 7 +- tests/middleware/tests.py | 6 +- tests/transactions/tests.py | 71 +++++- tests/transactions_regress/tests.py | 12 +- 14 files changed, 359 insertions(+), 269 deletions(-) (limited to 'docs/internals') diff --git a/django/db/__init__.py b/django/db/__init__.py index 13ba68ba7e..08c901ab7b 100644 --- a/django/db/__init__.py +++ b/django/db/__init__.py @@ -70,6 +70,7 @@ signals.request_started.connect(reset_queries) # their lifetime. NB: abort() doesn't do anything outside of a transaction. def close_old_connections(**kwargs): for conn in connections.all(): + # Remove this when the legacy transaction management goes away. try: conn.abort() except DatabaseError: diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 818850bf43..346d10198d 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -157,6 +157,7 @@ class BaseDatabaseWrapper(object): Commits a transaction and resets the dirty flag. """ self.validate_thread_sharing() + self.validate_no_atomic_block() self._commit() self.set_clean() @@ -165,6 +166,7 @@ class BaseDatabaseWrapper(object): Rolls back a transaction and resets the dirty flag. """ self.validate_thread_sharing() + self.validate_no_atomic_block() self._rollback() self.set_clean() @@ -265,6 +267,8 @@ class BaseDatabaseWrapper(object): If you switch off transaction management and there is a pending commit/rollback, the data will be commited, unless "forced" is True. """ + self.validate_no_atomic_block() + self.transaction_state.append(managed) if not managed and self.is_dirty() and not forced: @@ -280,6 +284,8 @@ class BaseDatabaseWrapper(object): over to the surrounding block, as a commit will commit all changes, even those from outside. (Commits are on connection level.) """ + self.validate_no_atomic_block() + if self.transaction_state: del self.transaction_state[-1] else: @@ -305,10 +311,19 @@ class BaseDatabaseWrapper(object): """ Enable or disable autocommit. """ + self.validate_no_atomic_block() self.ensure_connection() self._set_autocommit(autocommit) self.autocommit = autocommit + def validate_no_atomic_block(self): + """ + Raise an error if an atomic block is active. + """ + if self.in_atomic_block: + raise TransactionManagementError( + "This is forbidden when an 'atomic' block is active.") + def abort(self): """ Roll back any ongoing transaction and clean the transaction state diff --git a/django/db/transaction.py b/django/db/transaction.py index 8126c18a70..eb9d85e274 100644 --- a/django/db/transaction.py +++ b/django/db/transaction.py @@ -367,6 +367,9 @@ def autocommit(using=None): this decorator is useful if you globally activated transaction management in your settings file and want the default behavior in some view functions. """ + warnings.warn("autocommit is deprecated in favor of set_autocommit.", + PendingDeprecationWarning, stacklevel=2) + def entering(using): enter_transaction_management(managed=False, using=using) @@ -382,6 +385,9 @@ def commit_on_success(using=None): a rollback is made. This is one of the most common ways to do transaction control in Web apps. """ + warnings.warn("commit_on_success is deprecated in favor of atomic.", + PendingDeprecationWarning, stacklevel=2) + def entering(using): enter_transaction_management(using=using) @@ -409,6 +415,9 @@ def commit_manually(using=None): own -- it's up to the user to call the commit and rollback functions themselves. """ + warnings.warn("commit_manually is deprecated in favor of set_autocommit.", + PendingDeprecationWarning, stacklevel=2) + def entering(using): enter_transaction_management(using=using) @@ -420,10 +429,15 @@ def commit_manually(using=None): def commit_on_success_unless_managed(using=None): """ Transitory API to preserve backwards-compatibility while refactoring. + + Once the legacy transaction management is fully deprecated, this should + simply be replaced by atomic. Until then, it's necessary to avoid making a + commit where Django didn't use to, since entering atomic in managed mode + triggers a commmit. """ connection = get_connection(using) - if connection.autocommit and not connection.in_atomic_block: - return commit_on_success(using) + if connection.autocommit or connection.in_atomic_block: + return atomic(using) else: def entering(using): pass diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 1c8618713a..6c13af7ae4 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -329,6 +329,10 @@ these changes. 1.8 --- +* The decorators and context managers ``django.db.transaction.autocommit``, + ``commit_on_success`` and ``commit_manually`` will be removed. See + :ref:`transactions-upgrading-from-1.5`. + * The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their arguments. In 1.6 and 1.7, this behavior is provided by the version of these tags in the ``future`` template tag library. diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index ba8a4fc557..53d38a006b 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -105,16 +105,14 @@ you just won't get any of the nice new unittest2 features. Transaction context managers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Users of Python 2.5 and above may now use :ref:`transaction management functions -` as `context managers`_. For example:: +Users of Python 2.5 and above may now use transaction management functions as +`context managers`_. For example:: with transaction.autocommit(): # ... .. _context managers: http://docs.python.org/glossary.html#term-context-manager -For more information, see :ref:`transaction-management-functions`. - Configurable delete-cascade ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 4c8dd2f81f..582bceffca 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -148,16 +148,14 @@ you just won't get any of the nice new unittest2 features. Transaction context managers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Users of Python 2.5 and above may now use :ref:`transaction management functions -` as `context managers`_. For example:: +Users of Python 2.5 and above may now use transaction management functions as +`context managers`_. For example:: with transaction.autocommit(): # ... .. _context managers: http://docs.python.org/glossary.html#term-context-manager -For more information, see :ref:`transaction-management-functions`. - Configurable delete-cascade ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index c55ef0ef38..cc3bf94ef5 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -39,7 +39,7 @@ should improve performance. The existing APIs were deprecated, and new APIs were introduced, as described in :doc:`/topics/db/transactions`. Please review carefully the list of :ref:`known backwards-incompatibilities -` to determine if you need to make changes in +` to determine if you need to make changes in your code. Persistent database connections @@ -163,7 +163,7 @@ Backwards incompatible changes in 1.6 * Database-level autocommit is enabled by default in Django 1.6. While this doesn't change the general spirit of Django's transaction management, there are a few known backwards-incompatibities, described in the :ref:`transaction - management docs `. You should review your code + management docs `. You should review your code to determine if you're affected. * In previous versions, database-level autocommit was only an option for @@ -256,6 +256,19 @@ Backwards incompatible changes in 1.6 Features deprecated in 1.6 ========================== +Transaction management APIs +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Transaction management was completely overhauled in Django 1.6, and the +current APIs are deprecated: + +- :func:`django.db.transaction.autocommit` +- :func:`django.db.transaction.commit_on_success` +- :func:`django.db.transaction.commit_manually` + +The reasons for this change and the upgrade path are described in the +:ref:`transactions documentation `. + Changes to :ttag:`cycle` and :ttag:`firstof` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 2a4cd306c6..91b2cf41b3 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -24,7 +24,7 @@ immediately committed to the database. :ref:`See below for details .. versionchanged:: 1.6 Previous version of Django featured :ref:`a more complicated default - behavior `. + behavior `. Tying transactions to HTTP requests ----------------------------------- @@ -89,7 +89,7 @@ Django provides a single API to control database transactions. database. If this argument isn't provided, Django uses the ``"default"`` database. - ``atomic`` is usable both as a decorator:: + ``atomic`` is usable both as a `decorator`_:: from django.db import transaction @@ -98,7 +98,7 @@ Django provides a single API to control database transactions. # This code executes inside a transaction. do_stuff() - and as a context manager:: + and as a `context manager`_:: from django.db import transaction @@ -110,6 +110,9 @@ Django provides a single API to control database transactions. # This code executes inside a transaction. do_more_stuff() + .. _decorator: http://docs.python.org/glossary.html#term-decorator + .. _context manager: http://docs.python.org/glossary.html#term-context-manager + Wrapping ``atomic`` in a try/except block allows for natural handling of integrity errors:: @@ -145,158 +148,116 @@ Django provides a single API to control database transactions. - releases or rolls back to the savepoint when exiting an inner block; - commits or rolls back the transaction when exiting the outermost block. -.. _transaction-management-functions: - -Controlling transaction management in views -=========================================== - -For most people, implicit request-based transactions work wonderfully. However, -if you need more fine-grained control over how transactions are managed, you can -use a set of functions in ``django.db.transaction`` to control transactions on a -per-function or per-code-block basis. - -These functions, described in detail below, can be used in two different ways: - -* As a decorator_ on a particular function. For example:: - - from django.db import transaction - - @transaction.commit_on_success - def viewfunc(request): - # ... - # this code executes inside a transaction - # ... - -* As a `context manager`_ around a particular block of code:: - - from django.db import transaction - - def viewfunc(request): - # ... - # this code executes using default transaction management - # ... - - with transaction.commit_on_success(): - # ... - # this code executes inside a transaction - # ... - -Both techniques work with all supported version of Python. +.. _topics-db-transactions-savepoints: -.. _decorator: http://docs.python.org/glossary.html#term-decorator -.. _context manager: http://docs.python.org/glossary.html#term-context-manager +Savepoints +========== -For maximum compatibility, all of the examples below show transactions using the -decorator syntax, but all of the follow functions may be used as context -managers, too. +A savepoint is a marker within a transaction that enables you to roll back +part of a transaction, rather than the full transaction. Savepoints are +available with the SQLite (≥ 3.6.8), PostgreSQL, Oracle and MySQL (when using +the InnoDB storage engine) backends. Other backends provide the savepoint +functions, but they're empty operations -- they don't actually do anything. -.. note:: +Savepoints aren't especially useful if you are using autocommit, the default +behavior of Django. However, once you open a transaction with :func:`atomic`, +you build up a series of database operations awaiting a commit or rollback. If +you issue a rollback, the entire transaction is rolled back. Savepoints +provide the ability to perform a fine-grained rollback, rather than the full +rollback that would be performed by ``transaction.rollback()``. - Although the examples below use view functions as examples, these - decorators and context managers can be used anywhere in your code - that you need to deal with transactions. +.. versionchanged:: 1.6 -.. _topics-db-transactions-autocommit: +When the :func:`atomic` decorator is nested, it creates a savepoint to allow +partial commit or rollback. You're strongly encouraged to use :func:`atomic` +rather than the functions described below, but they're still part of the +public API, and there's no plan to deprecate them. -.. function:: autocommit +Each of these functions takes a ``using`` argument which should be the name of +a database for which the behavior applies. If no ``using`` argument is +provided then the ``"default"`` database is used. - Use the ``autocommit`` decorator to switch a view function to Django's - default commit behavior. +Savepoints are controlled by three methods on the transaction object: - Example:: +.. method:: transaction.savepoint(using=None) - from django.db import transaction + Creates a new savepoint. This marks a point in the transaction that + is known to be in a "good" state. - @transaction.autocommit - def viewfunc(request): - .... + Returns the savepoint ID (sid). - @transaction.autocommit(using="my_other_database") - def viewfunc2(request): - .... +.. method:: transaction.savepoint_commit(sid, using=None) - Within ``viewfunc()``, transactions will be committed as soon as you call - ``model.save()``, ``model.delete()``, or any other function that writes to - the database. ``viewfunc2()`` will have this same behavior, but for the - ``"my_other_database"`` connection. + Updates the savepoint to include any operations that have been performed + since the savepoint was created, or since the last commit. -.. function:: commit_on_success +.. method:: transaction.savepoint_rollback(sid, using=None) - Use the ``commit_on_success`` decorator to use a single transaction for all - the work done in a function:: + Rolls the transaction back to the last point at which the savepoint was + committed. - from django.db import transaction +The following example demonstrates the use of savepoints:: - @transaction.commit_on_success - def viewfunc(request): - .... + from django.db import transaction - @transaction.commit_on_success(using="my_other_database") - def viewfunc2(request): - .... + # open a transaction + @transaction.atomic + def viewfunc(request): - If the function returns successfully, then Django will commit all work done - within the function at that point. If the function raises an exception, - though, Django will roll back the transaction. + a.save() + # transaction now contains a.save() -.. function:: commit_manually + sid = transaction.savepoint() - Use the ``commit_manually`` decorator if you need full control over - transactions. It tells Django you'll be managing the transaction on your - own. + b.save() + # transaction now contains a.save() and b.save() - Whether you are writing or simply reading from the database, you must - ``commit()`` or ``rollback()`` explicitly or Django will raise a - :exc:`TransactionManagementError` exception. This is required when reading - from the database because ``SELECT`` statements may call functions which - modify tables, and thus it is impossible to know if any data has been - modified. + if want_to_keep_b: + transaction.savepoint_commit(sid) + # open transaction still contains a.save() and b.save() + else: + transaction.savepoint_rollback(sid) + # open transaction now contains only a.save() - Manual transaction management looks like this:: +Autocommit +========== - from django.db import transaction +.. _autocommit-details: - @transaction.commit_manually - def viewfunc(request): - ... - # You can commit/rollback however and whenever you want - transaction.commit() - ... +Why Django uses autocommit +-------------------------- - # But you've got to remember to do it yourself! - try: - ... - except: - transaction.rollback() - else: - transaction.commit() +In the SQL standards, each SQL query starts a transaction, unless one is +already in progress. Such transactions must then be committed or rolled back. - @transaction.commit_manually(using="my_other_database") - def viewfunc2(request): - .... +This isn't always convenient for application developers. To alleviate this +problem, most databases provide an autocommit mode. When autocommit is turned +on, each SQL query is wrapped in its own transaction. In other words, the +transaction is not only automatically started, but also automatically +committed. -.. _topics-db-transactions-requirements: +:pep:`249`, the Python Database API Specification v2.0, requires autocommit to +be initially turned off. Django overrides this default and turns autocommit +on. -Requirements for transaction handling -===================================== +To avoid this, you can :ref:`deactivate the transaction management +`, but it isn't recommended. -Django requires that every transaction that is opened is closed before the -completion of a request. +.. versionchanged:: 1.6 + Before Django 1.6, autocommit was turned off, and it was emulated by + forcing a commit after write operations in the ORM. -If you are using :func:`autocommit` (the default commit mode) or -:func:`commit_on_success`, this will be done for you automatically. However, -if you are manually managing transactions (using the :func:`commit_manually` -decorator), you must ensure that the transaction is either committed or rolled -back before a request is completed. +.. warning:: -This applies to all database operations, not just write operations. Even -if your transaction only reads from the database, the transaction must -be committed or rolled back before you complete a request. + If you're using the database API directly — for instance, you're running + SQL queries with ``cursor.execute()`` — be aware that autocommit is on, + and consider wrapping your operations in a transaction, with + :func:`atomic`, to ensure consistency. .. _managing-autocommit: Managing autocommit -=================== +------------------- .. versionadded:: 1.6 @@ -310,10 +271,17 @@ database connection, if you need to. These functions take a ``using`` argument which should be the name of a database. If it isn't provided, Django uses the ``"default"`` database. +Autocommit is initially turned on. If you turn it off, it's your +responsibility to restore it. + +:func:`atomic` requires autocommit to be turned on; it will raise an exception +if autocommit is off. Django will also refuse to turn autocommit off when an +:func:`atomic` block is active, because that would break atomicity. + .. _deactivate-transaction-management: -How to globally deactivate transaction management -================================================= +Deactivating transaction management +----------------------------------- Control freaks can totally disable all transaction management by setting :setting:`TRANSACTIONS_MANAGED` to ``True`` in the Django settings file. If @@ -328,71 +296,6 @@ something really strange. In almost all situations, you'll be better off using the default behavior, or the transaction middleware, and only modify selected functions as needed. -.. _topics-db-transactions-savepoints: - -Savepoints -========== - -A savepoint is a marker within a transaction that enables you to roll back -part of a transaction, rather than the full transaction. Savepoints are -available with the SQLite (≥ 3.6.8), PostgreSQL, Oracle and MySQL (when using -the InnoDB storage engine) backends. Other backends provide the savepoint -functions, but they're empty operations -- they don't actually do anything. - -Savepoints aren't especially useful if you are using the default -``autocommit`` behavior of Django. However, if you are using -``commit_on_success`` or ``commit_manually``, each open transaction will build -up a series of database operations, awaiting a commit or rollback. If you -issue a rollback, the entire transaction is rolled back. Savepoints provide -the ability to perform a fine-grained rollback, rather than the full rollback -that would be performed by ``transaction.rollback()``. - -Each of these functions takes a ``using`` argument which should be the name of -a database for which the behavior applies. If no ``using`` argument is -provided then the ``"default"`` database is used. - -Savepoints are controlled by three methods on the transaction object: - -.. method:: transaction.savepoint(using=None) - - Creates a new savepoint. This marks a point in the transaction that - is known to be in a "good" state. - - Returns the savepoint ID (sid). - -.. method:: transaction.savepoint_commit(sid, using=None) - - Updates the savepoint to include any operations that have been performed - since the savepoint was created, or since the last commit. - -.. method:: transaction.savepoint_rollback(sid, using=None) - - Rolls the transaction back to the last point at which the savepoint was - committed. - -The following example demonstrates the use of savepoints:: - - from django.db import transaction - - @transaction.commit_manually - def viewfunc(request): - - a.save() - # open transaction now contains a.save() - sid = transaction.savepoint() - - b.save() - # open transaction now contains a.save() and b.save() - - if want_to_keep_b: - transaction.savepoint_commit(sid) - # open transaction still contains a.save() and b.save() - else: - transaction.savepoint_rollback(sid) - # open transaction now contains only a.save() - - transaction.commit() - Database-specific notes ======================= @@ -477,45 +380,57 @@ transaction. For example:: In this example, ``a.save()`` will not be undone in the case where ``b.save()`` raises an exception. -Under the hood -============== +.. _transactions-upgrading-from-1.5: -.. _autocommit-details: +Changes from Django 1.5 and earlier +=================================== -Details on autocommit ---------------------- +The features described below were deprecated in Django 1.6 and will be removed +in Django 1.8. They're documented in order to ease the migration to the new +transaction management APIs. -In the SQL standards, each SQL query starts a transaction, unless one is -already in progress. Such transactions must then be committed or rolled back. +Legacy APIs +----------- -This isn't always convenient for application developers. To alleviate this -problem, most databases provide an autocommit mode. When autocommit is turned -on, each SQL query is wrapped in its own transaction. In other words, the -transaction is not only automatically started, but also automatically -committed. +The following functions, defined in ``django.db.transaction``, provided a way +to control transactions on a per-function or per-code-block basis. They could +be used as decorators or as context managers, and they accepted a ``using`` +argument, exactly like :func:`atomic`. -:pep:`249`, the Python Database API Specification v2.0, requires autocommit to -be initially turned off. Django overrides this default and turns autocommit -on. +.. function:: autocommit -To avoid this, you can :ref:`deactivate the transaction management -`, but it isn't recommended. + Enable Django's default autocommit behavior. -.. versionchanged:: 1.6 - Before Django 1.6, autocommit was turned off, and it was emulated by - forcing a commit after write operations in the ORM. + Transactions will be committed as soon as you call ``model.save()``, + ``model.delete()``, or any other function that writes to the database. -.. warning:: +.. function:: commit_on_success - If you're using the database API directly — for instance, you're running - SQL queries with ``cursor.execute()`` — be aware that autocommit is on, - and consider wrapping your operations in a transaction to ensure - consistency. + Use a single transaction for all the work done in a function. + + If the function returns successfully, then Django will commit all work done + within the function at that point. If the function raises an exception, + though, Django will roll back the transaction. + +.. function:: commit_manually + + Tells Django you'll be managing the transaction on your own. + + Whether you are writing or simply reading from the database, you must + ``commit()`` or ``rollback()`` explicitly or Django will raise a + :exc:`TransactionManagementError` exception. This is required when reading + from the database because ``SELECT`` statements may call functions which + modify tables, and thus it is impossible to know if any data has been + modified. .. _transaction-states: -Transaction management states ------------------------------ +Transaction states +------------------ + +The three functions described above relied on a concept called "transaction +states". This mechanisme was deprecated in Django 1.6, but it's still +available until Django 1.8.. At any time, each database connection is in one of these two states: @@ -529,35 +444,80 @@ Django starts in auto mode. ``TransactionMiddleware``, Internally, Django keeps a stack of states. Activations and deactivations must be balanced. -For example, at the beginning of each HTTP request, ``TransactionMiddleware`` -switches to managed mode; at the end of the request, it commits or rollbacks, +For example, ``commit_on_success`` switches to managed mode when entering the +block of code it controls; when exiting the block, it commits or rollbacks, and switches back to auto mode. -.. admonition:: Nesting decorators / context managers +So :func:`commit_on_success` really has two effects: it changes the +transaction state and it defines an transaction block. Nesting will give the +expected results in terms of transaction state, but not in terms of +transaction semantics. Most often, the inner block will commit, breaking the +atomicity of the outer block. - :func:`commit_on_success` has two effects: it changes the transaction - state, and defines an atomic transaction block. +:func:`autocommit` and :func:`commit_manually` have similar limitations. - Nesting with :func:`autocommit` and :func:`commit_manually` will give the - expected results in terms of transaction state, but not in terms of - transaction semantics. Most often, the inner block will commit, breaking - the atomicity of the outer block. +API changes +----------- -Django currently doesn't provide any APIs to create transactions in auto mode. +Managing transactions +~~~~~~~~~~~~~~~~~~~~~ -.. _transactions-changes-from-1.5: +Starting with Django 1.6, :func:`atomic` is the only supported API for +defining a transaction. Unlike the deprecated APIs, it's nestable and always +guarantees atomicity. -Changes from Django 1.5 and earlier -=================================== +In most cases, it will be a drop-in replacement for :func:`commit_on_success`. -Since version 1.6, Django uses database-level autocommit in auto mode. +During the deprecation period, it's possible to use :func:`atomic` within +:func:`autocommit`, :func:`commit_on_success` or :func:`commit_manually`. +However, the reverse is forbidden, because nesting the old decorators / +context managers breaks atomicity. + +If you enter :func:`atomic` while you're in managed mode, it will trigger a +commit to start from a clean slate. + +Managing autocommit +~~~~~~~~~~~~~~~~~~~ + +Django 1.6 introduces an explicit :ref:`API for mananging autocommit +`. + +To disable autocommit temporarily, instead of:: + with transaction.commit_manually(): + # do stuff + +you should now use:: + + transaction.set_autocommit(autocommit=False) + try: + # do stuff + finally: + transaction.set_autocommit(autocommit=True) + +To enable autocommit temporarily, instead of:: + + with transaction.autocommit(): + # do stuff + +you should now use:: + + transaction.set_autocommit(autocommit=True) + try: + # do stuff + finally: + transaction.set_autocommit(autocommit=False) + +Backwards incompatibilities +--------------------------- + +Since version 1.6, Django uses database-level autocommit in auto mode. Previously, it implemented application-level autocommit by triggering a commit after each ORM write. -As a consequence, each database query (for instance, an -ORM read) started a transaction that lasted until the next ORM write. Such -"automatic transactions" no longer exist in Django 1.6. +As a consequence, each database query (for instance, an ORM read) started a +transaction that lasted until the next ORM write. Such "automatic +transactions" no longer exist in Django 1.6. There are four known scenarios where this is backwards-incompatible. @@ -565,7 +525,7 @@ Note that managed mode isn't affected at all. This section assumes auto mode. See the :ref:`description of modes ` above. Sequences of custom SQL queries -------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you're executing several :ref:`custom SQL queries ` in a row, each one now runs in its own transaction, instead of sharing the @@ -577,20 +537,20 @@ usually followed by a call to ``transaction.commit_unless_managed``, which isn't necessary any more and should be removed. Select for update ------------------ +~~~~~~~~~~~~~~~~~ If you were relying on "automatic transactions" to provide locking between :meth:`~django.db.models.query.QuerySet.select_for_update` and a subsequent write operation — an extremely fragile design, but nonetheless possible — you -must wrap the relevant code in :func:`commit_on_success`. +must wrap the relevant code in :func:`atomic`. Using a high isolation level ----------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you were using the "repeatable read" isolation level or higher, and if you relied on "automatic transactions" to guarantee consistency between successive -reads, the new behavior is backwards-incompatible. To maintain consistency, -you must wrap such sequences in :func:`commit_on_success`. +reads, the new behavior might be backwards-incompatible. To enforce +consistency, you must wrap such sequences in :func:`atomic`. MySQL defaults to "repeatable read" and SQLite to "serializable"; they may be affected by this problem. @@ -602,10 +562,9 @@ PostgreSQL and Oracle default to "read committed" and aren't affected, unless you changed the isolation level. Using unsupported database features ------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ With triggers, views, or functions, it's possible to make ORM reads result in database modifications. Django 1.5 and earlier doesn't deal with this case and it's theoretically possible to observe a different behavior after upgrading to -Django 1.6 or later. In doubt, use :func:`commit_on_success` to enforce -integrity. +Django 1.6 or later. In doubt, use :func:`atomic` to enforce integrity. diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 5c8a8955eb..51acbcb07f 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -522,7 +522,8 @@ class FkConstraintsTests(TransactionTestCase): """ When constraint checks are disabled, should be able to write bad data without IntegrityErrors. """ - with transaction.commit_manually(): + transaction.set_autocommit(autocommit=False) + try: # Create an Article. models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) # Retrive it from the DB @@ -536,12 +537,15 @@ class FkConstraintsTests(TransactionTestCase): self.fail("IntegrityError should not have occurred.") finally: transaction.rollback() + finally: + transaction.set_autocommit(autocommit=True) def test_disable_constraint_checks_context_manager(self): """ When constraint checks are disabled (using context manager), should be able to write bad data without IntegrityErrors. """ - with transaction.commit_manually(): + transaction.set_autocommit(autocommit=False) + try: # Create an Article. models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) # Retrive it from the DB @@ -554,12 +558,15 @@ class FkConstraintsTests(TransactionTestCase): self.fail("IntegrityError should not have occurred.") finally: transaction.rollback() + finally: + transaction.set_autocommit(autocommit=True) def test_check_constraints(self): """ Constraint checks should raise an IntegrityError when bad data is in the DB. """ - with transaction.commit_manually(): + try: + transaction.set_autocommit(autocommit=False) # Create an Article. models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) # Retrive it from the DB @@ -572,6 +579,8 @@ class FkConstraintsTests(TransactionTestCase): connection.check_constraints() finally: transaction.rollback() + finally: + transaction.set_autocommit(autocommit=True) class ThreadTests(TestCase): diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index d147fe68a7..894a6c7fde 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -25,7 +25,8 @@ class SampleTestCase(TestCase): class TestNoInitialDataLoading(TransactionTestCase): def test_syncdb(self): - with transaction.commit_manually(): + transaction.set_autocommit(autocommit=False) + try: Book.objects.all().delete() management.call_command( @@ -35,6 +36,9 @@ class TestNoInitialDataLoading(TransactionTestCase): ) self.assertQuerysetEqual(Book.objects.all(), []) transaction.rollback() + finally: + transaction.set_autocommit(autocommit=True) + def test_flush(self): # Test presence of fixture (flush called by TransactionTestCase) @@ -45,7 +49,8 @@ class TestNoInitialDataLoading(TransactionTestCase): lambda a: a.name ) - with transaction.commit_manually(): + transaction.set_autocommit(autocommit=False) + try: management.call_command( 'flush', verbosity=0, @@ -55,6 +60,8 @@ class TestNoInitialDataLoading(TransactionTestCase): ) self.assertQuerysetEqual(Book.objects.all(), []) transaction.rollback() + finally: + transaction.set_autocommit(autocommit=True) class FixtureTestCase(TestCase): diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index 61dc4460df..f965dd81ac 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -684,5 +684,8 @@ class TestTicket11101(TransactionTestCase): @skipUnlessDBFeature('supports_transactions') def test_ticket_11101(self): """Test that fixtures can be rolled back (ticket #11101).""" - ticket_11101 = transaction.commit_manually(self.ticket_11101) - ticket_11101() + transaction.set_autocommit(autocommit=False) + try: + self.ticket_11101() + finally: + transaction.set_autocommit(autocommit=True) diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index e704fce342..7e26037967 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -24,6 +24,8 @@ from django.utils.encoding import force_str from django.utils.six.moves import xrange from django.utils.unittest import expectedFailure +from transactions.tests import IgnorePendingDeprecationWarningsMixin + from .models import Band @@ -670,11 +672,12 @@ class ETagGZipMiddlewareTest(TestCase): self.assertNotEqual(gzip_etag, nogzip_etag) -class TransactionMiddlewareTest(TransactionTestCase): +class TransactionMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Test the transaction middleware. """ def setUp(self): + super(TransactionMiddlewareTest, self).setUp() self.request = HttpRequest() self.request.META = { 'SERVER_NAME': 'testserver', @@ -686,6 +689,7 @@ class TransactionMiddlewareTest(TransactionTestCase): def tearDown(self): transaction.abort() + super(TransactionMiddlewareTest, self).tearDown() def test_request(self): TransactionMiddleware().process_request(self.request) diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index 14252dd6dc..d6cfd8ae95 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -1,9 +1,10 @@ from __future__ import absolute_import import sys +import warnings from django.db import connection, transaction, IntegrityError -from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature +from django.test import TransactionTestCase, skipUnlessDBFeature from django.utils import six from django.utils.unittest import skipUnless @@ -158,7 +159,69 @@ class AtomicInsideTransactionTests(AtomicTests): self.atomic.__exit__(*sys.exc_info()) -class TransactionTests(TransactionTestCase): +class AtomicInsideLegacyTransactionManagementTests(AtomicTests): + + def setUp(self): + transaction.enter_transaction_management() + + def tearDown(self): + # The tests access the database after exercising 'atomic', making the + # connection dirty; a rollback is required to make it clean. + transaction.rollback() + transaction.leave_transaction_management() + + +@skipUnless(connection.features.uses_savepoints, + "'atomic' requires transactions and savepoints.") +class AtomicErrorsTests(TransactionTestCase): + + def test_atomic_requires_autocommit(self): + transaction.set_autocommit(autocommit=False) + try: + with self.assertRaises(transaction.TransactionManagementError): + with transaction.atomic(): + pass + finally: + transaction.set_autocommit(autocommit=True) + + def test_atomic_prevents_disabling_autocommit(self): + autocommit = transaction.get_autocommit() + with transaction.atomic(): + with self.assertRaises(transaction.TransactionManagementError): + transaction.set_autocommit(autocommit=not autocommit) + # Make sure autocommit wasn't changed. + self.assertEqual(connection.autocommit, autocommit) + + def test_atomic_prevents_calling_transaction_methods(self): + with transaction.atomic(): + with self.assertRaises(transaction.TransactionManagementError): + transaction.commit() + with self.assertRaises(transaction.TransactionManagementError): + transaction.rollback() + + def test_atomic_prevents_calling_transaction_management_methods(self): + with transaction.atomic(): + with self.assertRaises(transaction.TransactionManagementError): + transaction.enter_transaction_management() + with self.assertRaises(transaction.TransactionManagementError): + transaction.leave_transaction_management() + + +class IgnorePendingDeprecationWarningsMixin(object): + + def setUp(self): + super(IgnorePendingDeprecationWarningsMixin, self).setUp() + self.catch_warnings = warnings.catch_warnings() + self.catch_warnings.__enter__() + warnings.filterwarnings("ignore", category=PendingDeprecationWarning) + + def tearDown(self): + self.catch_warnings.__exit__(*sys.exc_info()) + super(IgnorePendingDeprecationWarningsMixin, self).tearDown() + + +class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): + def create_a_reporter_then_fail(self, first, last): a = Reporter(first_name=first, last_name=last) a.save() @@ -313,7 +376,7 @@ class TransactionTests(TransactionTestCase): ) -class TransactionRollbackTests(TransactionTestCase): +class TransactionRollbackTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def execute_bad_sql(self): cursor = connection.cursor() cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") @@ -330,7 +393,7 @@ class TransactionRollbackTests(TransactionTestCase): self.assertRaises(IntegrityError, execute_bad_sql) transaction.rollback() -class TransactionContextManagerTests(TransactionTestCase): +class TransactionContextManagerTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def create_reporter_and_fail(self): Reporter.objects.create(first_name="Bob", last_name="Holtzman") raise Exception diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index e86db4d0aa..d5ee62da5e 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -6,10 +6,12 @@ from django.test import TransactionTestCase, skipUnlessDBFeature from django.test.utils import override_settings from django.utils.unittest import skipIf, skipUnless, expectedFailure +from transactions.tests import IgnorePendingDeprecationWarningsMixin + from .models import Mod, M2mA, M2mB -class TestTransactionClosing(TransactionTestCase): +class TestTransactionClosing(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Tests to make sure that transactions are properly closed when they should be, and aren't left pending after operations @@ -166,7 +168,7 @@ class TestTransactionClosing(TransactionTestCase): (connection.settings_dict['NAME'] == ':memory:' or not connection.settings_dict['NAME']), 'Test uses multiple connections, but in-memory sqlite does not support this') -class TestNewConnection(TransactionTestCase): +class TestNewConnection(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Check that new connections don't have special behaviour. """ @@ -211,7 +213,7 @@ class TestNewConnection(TransactionTestCase): @skipUnless(connection.vendor == 'postgresql', "This test only valid for PostgreSQL") -class TestPostgresAutocommitAndIsolation(TransactionTestCase): +class TestPostgresAutocommitAndIsolation(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Tests to make sure psycopg2's autocommit mode and isolation level is restored after entering and leaving transaction management. @@ -292,7 +294,7 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase): self.assertTrue(connection.autocommit) -class TestManyToManyAddTransaction(TransactionTestCase): +class TestManyToManyAddTransaction(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def test_manyrelated_add_commit(self): "Test for https://code.djangoproject.com/ticket/16818" a = M2mA.objects.create() @@ -307,7 +309,7 @@ class TestManyToManyAddTransaction(TransactionTestCase): self.assertEqual(a.others.count(), 1) -class SavepointTest(TransactionTestCase): +class SavepointTest(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): @skipIf(connection.vendor == 'sqlite', "SQLite doesn't support savepoints in managed mode") -- cgit v1.3 From ac37ed21b3d66dde1748f6edf3279656b0267b70 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 6 Mar 2013 11:12:24 +0100 Subject: Deprecated TransactionMiddleware and TRANSACTIONS_MANAGED. Replaced them with per-database options, for proper multi-db support. Also toned down the recommendation to tie transactions to HTTP requests. Thanks Jeremy for sharing his experience. --- django/core/handlers/base.py | 12 +++- django/db/backends/__init__.py | 4 +- django/db/utils.py | 8 +++ django/middleware/transaction.py | 13 +++- docs/internals/deprecation.txt | 11 +++- docs/ref/middleware.txt | 4 ++ docs/ref/settings.txt | 30 +++++++++ docs/releases/1.6.txt | 8 ++- docs/topics/db/transactions.txt | 134 +++++++++++++++++++++++++++------------ tests/handlers/tests.py | 30 ++++++++- tests/handlers/urls.py | 9 ++- tests/handlers/views.py | 17 +++++ 12 files changed, 223 insertions(+), 57 deletions(-) create mode 100644 tests/handlers/views.py (limited to 'docs/internals') diff --git a/django/core/handlers/base.py b/django/core/handlers/base.py index 0dcd9794c7..5327ce5891 100644 --- a/django/core/handlers/base.py +++ b/django/core/handlers/base.py @@ -6,10 +6,10 @@ import types from django import http from django.conf import settings -from django.core import exceptions from django.core import urlresolvers from django.core import signals from django.core.exceptions import MiddlewareNotUsed, PermissionDenied +from django.db import connections, transaction from django.utils.encoding import force_text from django.utils.module_loading import import_by_path from django.utils import six @@ -65,6 +65,13 @@ class BaseHandler(object): # as a flag for initialization being complete. self._request_middleware = request_middleware + def make_view_atomic(self, view): + if getattr(view, 'transactions_per_request', True): + for db in connections.all(): + if db.settings_dict['ATOMIC_REQUESTS']: + view = transaction.atomic(using=db.alias)(view) + return view + def get_response(self, request): "Returns an HttpResponse object for the given HttpRequest" try: @@ -101,8 +108,9 @@ class BaseHandler(object): break if response is None: + wrapped_callback = self.make_view_atomic(callback) try: - response = callback(request, *callback_args, **callback_kwargs) + response = wrapped_callback(request, *callback_args, **callback_kwargs) except Exception as e: # If the view raised an exception, run it through exception # middleware, and if the exception middleware returns a diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index 68551aad51..2cf75bd528 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -104,7 +104,7 @@ class BaseDatabaseWrapper(object): conn_params = self.get_connection_params() self.connection = self.get_new_connection(conn_params) self.init_connection_state() - if not settings.TRANSACTIONS_MANAGED: + if self.settings_dict['AUTOCOMMIT']: self.set_autocommit() connection_created.send(sender=self.__class__, connection=self) @@ -299,7 +299,7 @@ class BaseDatabaseWrapper(object): if self.transaction_state: managed = self.transaction_state[-1] else: - managed = settings.TRANSACTIONS_MANAGED + managed = not self.settings_dict['AUTOCOMMIT'] if self._dirty: self.rollback() diff --git a/django/db/utils.py b/django/db/utils.py index 71b89f93fb..936b42039d 100644 --- a/django/db/utils.py +++ b/django/db/utils.py @@ -2,6 +2,7 @@ from functools import wraps import os import pkgutil from threading import local +import warnings from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -158,6 +159,13 @@ class ConnectionHandler(object): except KeyError: raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias) + conn.setdefault('ATOMIC_REQUESTS', False) + if settings.TRANSACTIONS_MANAGED: + warnings.warn( + "TRANSACTIONS_MANAGED is deprecated. Use AUTOCOMMIT instead.", + PendingDeprecationWarning, stacklevel=2) + conn.setdefault('AUTOCOMMIT', False) + conn.setdefault('AUTOCOMMIT', True) conn.setdefault('ENGINE', 'django.db.backends.dummy') if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']: conn['ENGINE'] = 'django.db.backends.dummy' diff --git a/django/middleware/transaction.py b/django/middleware/transaction.py index 35f765d99f..95cc9a21f3 100644 --- a/django/middleware/transaction.py +++ b/django/middleware/transaction.py @@ -1,4 +1,7 @@ -from django.db import transaction +import warnings + +from django.core.exceptions import MiddlewareNotUsed +from django.db import connection, transaction class TransactionMiddleware(object): """ @@ -7,6 +10,14 @@ class TransactionMiddleware(object): commit, the commit is done when a successful response is created. If an exception happens, the database is rolled back. """ + + def __init__(self): + warnings.warn( + "TransactionMiddleware is deprecated in favor of ATOMIC_REQUESTS.", + PendingDeprecationWarning, stacklevel=2) + if connection.settings_dict['ATOMIC_REQUESTS']: + raise MiddlewareNotUsed + def process_request(self, request): """Enters transaction management""" transaction.enter_transaction_management() diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 6c13af7ae4..19675801e4 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -329,9 +329,14 @@ these changes. 1.8 --- -* The decorators and context managers ``django.db.transaction.autocommit``, - ``commit_on_success`` and ``commit_manually`` will be removed. See - :ref:`transactions-upgrading-from-1.5`. +* The following transaction management APIs will be removed: + + - ``TransactionMiddleware``, + - the decorators and context managers ``autocommit``, ``commit_on_success``, + and ``commit_manually``, + - the ``TRANSACTIONS_MANAGED`` setting. + + Upgrade paths are described in :ref:`transactions-upgrading-from-1.5`. * The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their arguments. In 1.6 and 1.7, this behavior is provided by the version of these diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index 1e6e57f720..20bb2fb751 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -205,6 +205,10 @@ Transaction middleware .. class:: TransactionMiddleware +.. versionchanged:: 1.6 + ``TransactionMiddleware`` is deprecated. The documentation of transactions + contains :ref:`upgrade instructions `. + Binds commit and rollback of the default database to the request/response phase. If a view function runs successfully, a commit is done. If it fails with an exception, a rollback is done. diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 0cd141bcef..2b80527d8b 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -408,6 +408,30 @@ SQLite. This can be configured using the following:: For other database backends, or more complex SQLite configurations, other options will be required. The following inner options are available. +.. setting:: DATABASE-ATOMIC_REQUESTS + +ATOMIC_REQUESTS +~~~~~~~~~~~~~~~ + +.. versionadded:: 1.6 + +Default: ``False`` + +Set this to ``True`` to wrap each HTTP request in a transaction on this +database. See :ref:`tying-transactions-to-http-requests`. + +.. setting:: DATABASE-AUTOCOMMIT + +AUTOCOMMIT +~~~~~~~~~~ + +.. versionadded:: 1.6 + +Default: ``True`` + +Set this to ``False`` if you want to :ref:`disable Django's transaction +management ` and implement your own. + .. setting:: DATABASE-ENGINE ENGINE @@ -1807,6 +1831,12 @@ to ensure your processes are running in the correct environment. TRANSACTIONS_MANAGED -------------------- +.. deprecated:: 1.6 + + This setting was deprecated because its name is very misleading. Use the + :setting:`AUTOCOMMIT ` key in :setting:`DATABASES` + entries instead. + Default: ``False`` Set this to ``True`` if you want to :ref:`disable Django's transaction diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index cc3bf94ef5..a1fe69229c 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -262,9 +262,11 @@ Transaction management APIs Transaction management was completely overhauled in Django 1.6, and the current APIs are deprecated: -- :func:`django.db.transaction.autocommit` -- :func:`django.db.transaction.commit_on_success` -- :func:`django.db.transaction.commit_manually` +- ``django.middleware.transaction.TransactionMiddleware`` +- ``django.db.transaction.autocommit`` +- ``django.db.transaction.commit_on_success`` +- ``django.db.transaction.commit_manually`` +- the ``TRANSACTIONS_MANAGED`` setting The reasons for this change and the upgrade path are described in the :ref:`transactions documentation `. diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 91b2cf41b3..37a369a02f 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -26,45 +26,61 @@ immediately committed to the database. :ref:`See below for details Previous version of Django featured :ref:`a more complicated default behavior `. +.. _tying-transactions-to-http-requests: + Tying transactions to HTTP requests ----------------------------------- -The recommended way to handle transactions in Web requests is to tie them to -the request and response phases via Django's ``TransactionMiddleware``. +A common way to handle transactions on the web is to wrap each request in a +transaction. Set :setting:`ATOMIC_REQUESTS ` to +``True`` in the configuration of each database for which you want to enable +this behavior. It works like this. When a request starts, Django starts a transaction. If the -response is produced without problems, Django commits any pending transactions. -If the view function produces an exception, Django rolls back any pending -transactions. - -To activate this feature, just add the ``TransactionMiddleware`` middleware to -your :setting:`MIDDLEWARE_CLASSES` setting:: - - MIDDLEWARE_CLASSES = ( - 'django.middleware.cache.UpdateCacheMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.transaction.TransactionMiddleware', - 'django.middleware.cache.FetchFromCacheMiddleware', - ) - -The order is quite important. The transaction middleware applies not only to -view functions, but also for all middleware modules that come after it. So if -you use the session middleware after the transaction middleware, session -creation will be part of the transaction. - -The various cache middlewares are an exception: ``CacheMiddleware``, -:class:`~django.middleware.cache.UpdateCacheMiddleware`, and -:class:`~django.middleware.cache.FetchFromCacheMiddleware` are never affected. -Even when using database caching, Django's cache backend uses its own database -connection internally. - -.. note:: - - The ``TransactionMiddleware`` only affects the database aliased - as "default" within your :setting:`DATABASES` setting. If you are using - multiple databases and want transaction control over databases other than - "default", you will need to write your own transaction middleware. +response is produced without problems, Django commits the transaction. If the +view function produces an exception, Django rolls back the transaction. +Middleware always runs outside of this transaction. + +You may perfom partial commits and rollbacks in your view code, typically with +the :func:`atomic` context manager. However, at the end of the view, either +all the changes will be committed, or none of them. + +To disable this behavior for a specific view, you must set the +``transactions_per_request`` attribute of the view function itself to +``False``, like this:: + + def my_view(request): + do_stuff() + my_view.transactions_per_request = False + +.. warning:: + + While the simplicity of this transaction model is appealing, it also makes it + inefficient when traffic increases. Opening a transaction for every view has + some overhead. The impact on performance depends on the query patterns of your + application and on how well your database handles locking. + +.. admonition:: Per-request transactions and streaming responses + + When a view returns a :class:`~django.http.StreamingHttpResponse`, reading + the contents of the response will often execute code to generate the + content. Since the view has already returned, such code runs outside of + the transaction. + + Generally speaking, it isn't advisable to write to the database while + generating a streaming response, since there's no sensible way to handle + errors after starting to send the response. + +In practice, this feature simply wraps every view function in the :func:`atomic` +decorator described below. + +Note that only the execution of your view in enclosed in the transactions. +Middleware run outside of the transaction, and so does the rendering of +template responses. + +.. versionchanged:: 1.6 + Django used to provide this feature via ``TransactionMiddleware``, which is + now deprecated. Controlling transactions explicitly ----------------------------------- @@ -283,18 +299,20 @@ if autocommit is off. Django will also refuse to turn autocommit off when an Deactivating transaction management ----------------------------------- -Control freaks can totally disable all transaction management by setting -:setting:`TRANSACTIONS_MANAGED` to ``True`` in the Django settings file. If -you do this, Django won't enable autocommit. You'll get the regular behavior -of the underlying database library. +You can totally disable Django's transaction management for a given database +by setting :setting:`AUTOCOMMIT ` to ``False`` in its +configuration. If you do this, Django won't enable autocommit, and won't +perform any commits. You'll get the regular behavior of the underlying +database library. This requires you to commit explicitly every transaction, even those started by Django or by third-party libraries. Thus, this is best used in situations where you want to run your own transaction-controlling middleware or do something really strange. -In almost all situations, you'll be better off using the default behavior, or -the transaction middleware, and only modify selected functions as needed. +.. versionchanged:: 1.6 + This used to be controlled by the ``TRANSACTIONS_MANAGED`` setting. + Database-specific notes ======================= @@ -459,6 +477,35 @@ atomicity of the outer block. API changes ----------- +Transaction middleware +~~~~~~~~~~~~~~~~~~~~~~ + +In Django 1.6, ``TransactionMiddleware`` is deprecated and replaced +:setting:`ATOMIC_REQUESTS `. While the general +behavior is the same, there are a few differences. + +With the transaction middleware, it was still possible to switch to autocommit +or to commit explicitly in a view. Since :func:`atomic` guarantees atomicity, +this isn't allowed any longer. + +To avoid wrapping a particular view in a transaction, instead of:: + + @transaction.autocommit + def my_view(request): + do_stuff() + +you must now use this pattern:: + + def my_view(request): + do_stuff() + my_view.transactions_per_request = False + +The transaction middleware applied not only to view functions, but also to +middleware modules that come after it. For instance, if you used the session +middleware after the transaction middleware, session creation was part of the +transaction. :setting:`ATOMIC_REQUESTS ` only +applies to the view itself. + Managing transactions ~~~~~~~~~~~~~~~~~~~~~ @@ -508,6 +555,13 @@ you should now use:: finally: transaction.set_autocommit(autocommit=False) +Disabling transaction management +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instead of setting ``TRANSACTIONS_MANAGED = True``, set the ``AUTOCOMMIT`` key +to ``False`` in the configuration of each database, as explained in :ref +:`deactivate-transaction-management`. + Backwards incompatibilities --------------------------- diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 6eb9bd23fe..3680eecdd2 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -1,9 +1,8 @@ from django.core.handlers.wsgi import WSGIHandler from django.core.signals import request_started, request_finished -from django.db import close_old_connections -from django.test import RequestFactory, TestCase +from django.db import close_old_connections, connection +from django.test import RequestFactory, TestCase, TransactionTestCase from django.test.utils import override_settings -from django.utils import six class HandlerTests(TestCase): @@ -37,6 +36,31 @@ class HandlerTests(TestCase): self.assertEqual(response.status_code, 400) +class TransactionsPerRequestTests(TransactionTestCase): + urls = 'handlers.urls' + + def test_no_transaction(self): + response = self.client.get('/in_transaction/') + self.assertContains(response, 'False') + + def test_auto_transaction(self): + old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS'] + try: + connection.settings_dict['ATOMIC_REQUESTS'] = True + response = self.client.get('/in_transaction/') + finally: + connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests + self.assertContains(response, 'True') + + def test_no_auto_transaction(self): + old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS'] + try: + connection.settings_dict['ATOMIC_REQUESTS'] = True + response = self.client.get('/not_in_transaction/') + finally: + connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests + self.assertContains(response, 'False') + class SignalsTests(TestCase): urls = 'handlers.urls' diff --git a/tests/handlers/urls.py b/tests/handlers/urls.py index 8570f04696..29858055ab 100644 --- a/tests/handlers/urls.py +++ b/tests/handlers/urls.py @@ -1,9 +1,12 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url -from django.http import HttpResponse, StreamingHttpResponse + +from . import views urlpatterns = patterns('', - url(r'^regular/$', lambda request: HttpResponse(b"regular content")), - url(r'^streaming/$', lambda request: StreamingHttpResponse([b"streaming", b" ", b"content"])), + url(r'^regular/$', views.regular), + url(r'^streaming/$', views.streaming), + url(r'^in_transaction/$', views.in_transaction), + url(r'^not_in_transaction/$', views.not_in_transaction), ) diff --git a/tests/handlers/views.py b/tests/handlers/views.py new file mode 100644 index 0000000000..22d9ea4c7d --- /dev/null +++ b/tests/handlers/views.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals + +from django.db import connection +from django.http import HttpResponse, StreamingHttpResponse + +def regular(request): + return HttpResponse(b"regular content") + +def streaming(request): + return StreamingHttpResponse([b"streaming", b" ", b"content"]) + +def in_transaction(request): + return HttpResponse(str(connection.in_atomic_block)) + +def not_in_transaction(request): + return HttpResponse(str(connection.in_atomic_block)) +not_in_transaction.transactions_per_request = False -- cgit v1.3 From 571b2d139baa81ae9a0afea88c5b570a2d16d313 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sat, 9 Mar 2013 08:49:37 -0600 Subject: Deprecated django.contrib.comments. --- django/contrib/comments/__init__.py | 3 +++ docs/index.txt | 1 - docs/internals/deprecation.txt | 2 ++ docs/ref/contrib/comments/custom.txt | 12 ++++++++++++ docs/ref/contrib/comments/example.txt | 12 ++++++++++++ docs/ref/contrib/comments/forms.txt | 14 +++++++++++++- docs/ref/contrib/comments/index.txt | 12 ++++++++++++ docs/ref/contrib/comments/models.txt | 12 ++++++++++++ docs/ref/contrib/comments/moderation.txt | 12 ++++++++++++ docs/ref/contrib/comments/signals.txt | 12 ++++++++++++ docs/releases/1.6.txt | 13 +++++++++++++ 11 files changed, 103 insertions(+), 2 deletions(-) (limited to 'docs/internals') diff --git a/django/contrib/comments/__init__.py b/django/contrib/comments/__init__.py index 1798c1adb5..007b77ad7b 100644 --- a/django/contrib/comments/__init__.py +++ b/django/contrib/comments/__init__.py @@ -1,3 +1,4 @@ +import warnings from django.conf import settings from django.core import urlresolvers from django.core.exceptions import ImproperlyConfigured @@ -5,6 +6,8 @@ from django.contrib.comments.models import Comment from django.contrib.comments.forms import CommentForm from django.utils.importlib import import_module +warnings.warn("django.contrib.comments is deprecated and will be removed before Django 1.8.", PendingDeprecationWarning) + DEFAULT_COMMENTS_APP = 'django.contrib.comments' def get_comment_app(): diff --git a/docs/index.txt b/docs/index.txt index 73b378de4d..197856ea4b 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -254,7 +254,6 @@ applications: * :doc:`Logging ` * :doc:`Sending emails ` * :doc:`Syndication feeds (RSS/Atom) ` -* :doc:`Comments `, :doc:`comment moderation ` and :doc:`custom comments ` * :doc:`Pagination ` * :doc:`Messages framework ` * :doc:`Serialization ` diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 19675801e4..b9948affcb 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -365,6 +365,8 @@ these changes. - ``django.db.transaction.commit_unless_managed()`` - ``django.db.transaction.rollback_unless_managed()`` +* ``django.contrib.comments`` will be removed. + 2.0 --- diff --git a/docs/ref/contrib/comments/custom.txt b/docs/ref/contrib/comments/custom.txt index b4ab65bc2d..fd70a6a224 100644 --- a/docs/ref/contrib/comments/custom.txt +++ b/docs/ref/contrib/comments/custom.txt @@ -4,6 +4,18 @@ Customizing the comments framework .. currentmodule:: django.contrib.comments +.. warning:: + + Django's comment framework has been deprecated and is no longer supported. + Most users will be better served with a custom solution, or a hosted + product like Disqus__. + + The code formerly known as ``django.contrib.comments`` is `still available + in an external repository`__. + + __ https://disqus.com/ + __ https://github.com/django/django-contrib-comments + If the built-in comment framework doesn't quite fit your needs, you can extend the comment app's behavior to add custom data and logic. The comments framework lets you extend the built-in comment model, the built-in comment form, and the diff --git a/docs/ref/contrib/comments/example.txt b/docs/ref/contrib/comments/example.txt index e99c10f732..abf79c5f14 100644 --- a/docs/ref/contrib/comments/example.txt +++ b/docs/ref/contrib/comments/example.txt @@ -4,6 +4,18 @@ Example of using the built-in comments app =========================================== +.. warning:: + + Django's comment framework has been deprecated and is no longer supported. + Most users will be better served with a custom solution, or a hosted + product like Disqus__. + + The code formerly known as ``django.contrib.comments`` is `still available + in an external repository`__. + + __ https://disqus.com/ + __ https://github.com/django/django-contrib-comments + Follow the first three steps of the quick start guide in the :doc:`documentation `. diff --git a/docs/ref/contrib/comments/forms.txt b/docs/ref/contrib/comments/forms.txt index c21a27bb9e..f2624ca870 100644 --- a/docs/ref/contrib/comments/forms.txt +++ b/docs/ref/contrib/comments/forms.txt @@ -5,6 +5,18 @@ Comment form classes .. module:: django.contrib.comments.forms :synopsis: Forms for dealing with the built-in comment model. +.. warning:: + + Django's comment framework has been deprecated and is no longer supported. + Most users will be better served with a custom solution, or a hosted + product like Disqus__. + + The code formerly known as ``django.contrib.comments`` is `still available + in an external repository`__. + + __ https://disqus.com/ + __ https://github.com/django/django-contrib-comments + The ``django.contrib.comments.forms`` module contains a handful of forms you'll use when writing custom views dealing with comments, or when writing :doc:`custom comment apps `. @@ -43,4 +55,4 @@ forms that you can subclass to reuse pieces of the form handling logic: Handles the details of the comment itself. This class contains the ``name``, ``email``, ``url``, and the ``comment`` - field itself, along with the associated validation logic. \ No newline at end of file + field itself, along with the associated validation logic. diff --git a/docs/ref/contrib/comments/index.txt b/docs/ref/contrib/comments/index.txt index d4e967b4b2..6db69d8168 100644 --- a/docs/ref/contrib/comments/index.txt +++ b/docs/ref/contrib/comments/index.txt @@ -7,6 +7,18 @@ Django's comments framework .. highlightlang:: html+django +.. warning:: + + Django's comment framework has been deprecated and is no longer supported. + Most users will be better served with a custom solution, or a hosted + product like Disqus__. + + The code formerly known as ``django.contrib.comments`` is `still available + in an external repository`__. + + __ https://disqus.com/ + __ https://github.com/django/django-contrib-comments + Django includes a simple, yet customizable comments framework. The built-in comments framework can be used to attach comments to any model, so you can use it for comments on blog entries, photos, book chapters, or anything else. diff --git a/docs/ref/contrib/comments/models.txt b/docs/ref/contrib/comments/models.txt index 78e7b92145..cae9c11971 100644 --- a/docs/ref/contrib/comments/models.txt +++ b/docs/ref/contrib/comments/models.txt @@ -5,6 +5,18 @@ The built-in comment models .. module:: django.contrib.comments.models :synopsis: The built-in comment models +.. warning:: + + Django's comment framework has been deprecated and is no longer supported. + Most users will be better served with a custom solution, or a hosted + product like Disqus__. + + The code formerly known as ``django.contrib.comments`` is `still available + in an external repository`__. + + __ https://disqus.com/ + __ https://github.com/django/django-contrib-comments + .. class:: Comment Django's built-in comment model. Has the following fields: diff --git a/docs/ref/contrib/comments/moderation.txt b/docs/ref/contrib/comments/moderation.txt index a7138dda53..796e257200 100644 --- a/docs/ref/contrib/comments/moderation.txt +++ b/docs/ref/contrib/comments/moderation.txt @@ -5,6 +5,18 @@ Generic comment moderation .. module:: django.contrib.comments.moderation :synopsis: Support for automatic comment moderation. +.. warning:: + + Django's comment framework has been deprecated and is no longer supported. + Most users will be better served with a custom solution, or a hosted + product like Disqus__. + + The code formerly known as ``django.contrib.comments`` is `still available + in an external repository`__. + + __ https://disqus.com/ + __ https://github.com/django/django-contrib-comments + Django's bundled comments application is extremely useful on its own, but the amount of comment spam circulating on the Web today essentially makes it necessary to have some sort of automatic diff --git a/docs/ref/contrib/comments/signals.txt b/docs/ref/contrib/comments/signals.txt index ea901b6a95..f9df8980d7 100644 --- a/docs/ref/contrib/comments/signals.txt +++ b/docs/ref/contrib/comments/signals.txt @@ -5,6 +5,18 @@ Signals sent by the comments app .. module:: django.contrib.comments.signals :synopsis: Signals sent by the comment module. +.. warning:: + + Django's comment framework has been deprecated and is no longer supported. + Most users will be better served with a custom solution, or a hosted + product like Disqus__. + + The code formerly known as ``django.contrib.comments`` is `still available + in an external repository`__. + + __ https://disqus.com/ + __ https://github.com/django/django-contrib-comments + The comment app sends a series of :doc:`signals ` to allow for comment moderation and similar activities. See :doc:`the introduction to signals ` for information about how to register for and receive these diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index a1fe69229c..eec55c6632 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -271,6 +271,19 @@ current APIs are deprecated: The reasons for this change and the upgrade path are described in the :ref:`transactions documentation `. +``django.contrib.comments`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django's comment framework has been deprecated and is no longer supported. It +will be available in Django 1.6 and 1.7, and removed in Django 1.8. Most users +will be better served with a custom solution, or a hosted product like Disqus__. + +The code formerly known as ``django.contrib.comments`` is `still available +in an external repository`__. + +__ https://disqus.com/ +__ https://github.com/django/django-contrib-comments + Changes to :ttag:`cycle` and :ttag:`firstof` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- cgit v1.3 From 5d8342f321c1e1017e63555270999c9378f10185 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Wed, 13 Mar 2013 14:47:48 +0100 Subject: Proof-read and adjusted the transactions docs. --- docs/internals/deprecation.txt | 14 +++++---- docs/releases/1.6.txt | 3 +- docs/topics/db/transactions.txt | 70 +++++++++++++++++++---------------------- 3 files changed, 42 insertions(+), 45 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index b9948affcb..1305d68859 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -329,14 +329,19 @@ these changes. 1.8 --- +* ``django.contrib.comments`` will be removed. + * The following transaction management APIs will be removed: - ``TransactionMiddleware``, - the decorators and context managers ``autocommit``, ``commit_on_success``, - and ``commit_manually``, + and ``commit_manually``, defined in ``django.db.transaction``, + - the functions ``commit_unless_managed`` and ``rollback_unless_managed``, + also defined in ``django.db.transaction``, - the ``TRANSACTIONS_MANAGED`` setting. - Upgrade paths are described in :ref:`transactions-upgrading-from-1.5`. + Upgrade paths are described in the :ref:`transaction management docs + `. * The :ttag:`cycle` and :ttag:`firstof` template tags will auto-escape their arguments. In 1.6 and 1.7, this behavior is provided by the version of these @@ -358,14 +363,11 @@ these changes. ``ChangeList.root_query_set`` and ``ChangeList.query_set``. * The following private APIs will be removed: + - ``django.db.close_connection()`` - ``django.db.backends.creation.BaseDatabaseCreation.set_autocommit()`` - ``django.db.transaction.is_managed()`` - ``django.db.transaction.managed()`` - - ``django.db.transaction.commit_unless_managed()`` - - ``django.db.transaction.rollback_unless_managed()`` - -* ``django.contrib.comments`` will be removed. 2.0 --- diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index eec55c6632..30c3cc5d2c 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -36,7 +36,8 @@ Improved transaction management Django's transaction management was overhauled. Database-level autocommit is now turned on by default. This makes transaction handling more explicit and should improve performance. The existing APIs were deprecated, and new APIs -were introduced, as described in :doc:`/topics/db/transactions`. +were introduced, as described in the :doc:`transaction management docs +`. Please review carefully the list of :ref:`known backwards-incompatibilities ` to determine if you need to make changes in diff --git a/docs/topics/db/transactions.txt b/docs/topics/db/transactions.txt index 122af14359..697dee49c0 100644 --- a/docs/topics/db/transactions.txt +++ b/docs/topics/db/transactions.txt @@ -35,10 +35,10 @@ transaction. Set :setting:`ATOMIC_REQUESTS ` to ``True`` in the configuration of each database for which you want to enable this behavior. -It works like this. When a request starts, Django starts a transaction. If the -response is produced without problems, Django commits the transaction. If the -view function produces an exception, Django rolls back the transaction. -Middleware always runs outside of this transaction. +It works like this. Before calling a view function, Django starts a +transaction. If the response is produced without problems, Django commits the +transaction. If the view produces an exception, Django rolls back the +transaction. You may perfom partial commits and rollbacks in your view code, typically with the :func:`atomic` context manager. However, at the end of the view, either @@ -207,13 +207,6 @@ To avoid this, you can :ref:`deactivate the transaction management Before Django 1.6, autocommit was turned off, and it was emulated by forcing a commit after write operations in the ORM. -.. warning:: - - If you're using the database API directly — for instance, you're running - SQL queries with ``cursor.execute()`` — be aware that autocommit is on, - and consider wrapping your operations in a transaction, with - :func:`atomic`, to ensure consistency. - .. _deactivate-transaction-management: Deactivating transaction management @@ -251,8 +244,8 @@ Autocommit .. versionadded:: 1.6 -Django provides a straightforward API to manage the autocommit state of each -database connection, if you need to. +Django provides a straightforward API in the :mod:`django.db.transaction` +module to manage the autocommit state of each database connection. .. function:: get_autocommit(using=None) @@ -287,7 +280,7 @@ start a transaction is to disable autocommit with :func:`set_autocommit`. Once you're in a transaction, you can choose either to apply the changes you've performed until this point with :func:`commit`, or to cancel them with -:func:`rollback`. +:func:`rollback`. These functions are defined in :mod:`django.db.transaction`. .. function:: commit(using=None) @@ -332,10 +325,8 @@ Savepoints are controlled by three functions in :mod:`django.db.transaction`: .. function:: savepoint(using=None) - Creates a new savepoint. This marks a point in the transaction that - is known to be in a "good" state. - - Returns the savepoint ID (``sid``). + Creates a new savepoint. This marks a point in the transaction that is + known to be in a "good" state. Returns the savepoint ID (``sid``). .. function:: savepoint_commit(sid, using=None) @@ -388,11 +379,9 @@ While SQLite ≥ 3.6.8 supports savepoints, a flaw in the design of the :mod:`sqlite3` makes them hardly usable. When autocommit is enabled, savepoints don't make sense. When it's disabled, -:mod:`sqlite3` commits implicitly before savepoint-related statement. (It +:mod:`sqlite3` commits implicitly before savepoint statements. (In fact, it commits before any statement other than ``SELECT``, ``INSERT``, ``UPDATE``, -``DELETE`` and ``REPLACE``.) - -This has two consequences: +``DELETE`` and ``REPLACE``.) This bug has two consequences: - The low level APIs for savepoints are only usable inside a transaction ie. inside an :func:`atomic` block. @@ -407,22 +396,27 @@ depends on your MySQL version and the table types you're using. (By peculiarities are outside the scope of this article, but the MySQL site has `information on MySQL transactions`_. -If your MySQL setup does *not* support transactions, then Django will function -in autocommit mode: Statements will be executed and committed as soon as -they're called. If your MySQL setup *does* support transactions, Django will -handle transactions as explained in this document. +If your MySQL setup does *not* support transactions, then Django will always +function in autocommit mode: statements will be executed and committed as soon +as they're called. If your MySQL setup *does* support transactions, Django +will handle transactions as explained in this document. .. _information on MySQL transactions: http://dev.mysql.com/doc/refman/5.0/en/sql-syntax-transactions.html Handling exceptions within PostgreSQL transactions -------------------------------------------------- -When a call to a PostgreSQL cursor raises an exception (typically -``IntegrityError``), all subsequent SQL in the same transaction will fail with -the error "current transaction is aborted, queries ignored until end of -transaction block". Whilst simple use of ``save()`` is unlikely to raise an -exception in PostgreSQL, there are more advanced usage patterns which -might, such as saving objects with unique fields, saving using the +.. note:: + This section is relevant only if you're implementing your own transaction + management. This problem cannot occur in Django's default mode and + :func:`atomic` handles it automatically. + +Inside a transaction, when a call to a PostgreSQL cursor raises an exception +(typically ``IntegrityError``), all subsequent SQL in the same transaction +will fail with the error "current transaction is aborted, queries ignored +until end of transaction block". Whilst simple use of ``save()`` is unlikely +to raise an exception in PostgreSQL, there are more advanced usage patterns +which might, such as saving objects with unique fields, saving using the force_insert/force_update flag, or invoking custom SQL. There are several ways to recover from this sort of error. @@ -529,9 +523,9 @@ Django starts in auto mode. ``TransactionMiddleware``, Internally, Django keeps a stack of states. Activations and deactivations must be balanced. -For example, ``commit_on_success`` switches to managed mode when entering the -block of code it controls; when exiting the block, it commits or rollbacks, -and switches back to auto mode. +For example, :func:`commit_on_success` switches to managed mode when entering +the block of code it controls; when exiting the block, it commits or +rollbacks, and switches back to auto mode. So :func:`commit_on_success` really has two effects: it changes the transaction state and it defines an transaction block. Nesting will give the @@ -568,7 +562,7 @@ you must now use this pattern:: my_view.transactions_per_request = False The transaction middleware applied not only to view functions, but also to -middleware modules that come after it. For instance, if you used the session +middleware modules that came after it. For instance, if you used the session middleware after the transaction middleware, session creation was part of the transaction. :setting:`ATOMIC_REQUESTS ` only applies to the view itself. @@ -651,8 +645,8 @@ same "automatic transaction". If you need to enforce atomicity, you must wrap the sequence of queries in :func:`commit_on_success`. To check for this problem, look for calls to ``cursor.execute()``. They're -usually followed by a call to ``transaction.commit_unless_managed``, which -isn't necessary any more and should be removed. +usually followed by a call to ``transaction.commit_unless_managed()``, which +isn't useful any more and should be removed. Select for update ~~~~~~~~~~~~~~~~~ -- cgit v1.3 From b492e590746df51ddcdfa2a2372008455a457bcc Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 14 Mar 2013 14:59:15 +0100 Subject: Updated release instructions to account for website automation. --- docs/internals/howto-release-django.txt | 54 +++++++++++---------------------- 1 file changed, 17 insertions(+), 37 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 83b6a8c9be..b6f879753a 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -17,7 +17,7 @@ There are three types of releases that you might need to make * Security releases, disclosing and fixing a vulnerability. This'll generally involve two or three simultaneous releases -- e.g. - 1.5.X, 1.6.X, and, depending on timing, perhaps a 1.7 alpha/beta/rc. + 1.5.x, 1.6.x, and, depending on timing, perhaps a 1.7 alpha/beta/rc. * Regular version releases, either a final release (e.g. 1.5) or a bugfix update (e.g. 1.5.1). @@ -36,12 +36,11 @@ differences noted. The short version is: #. Update version numbers and create the release package(s)! -#. Upload the package(s) to the the ``djangoproject.com`` server and create - some redirects for download/checksum links. +#. Upload the package(s) to the ``djangoproject.com`` server. #. Unless this is a pre-release, add the new version(s) to PyPI. -#. Update the home page and download page to link to the new version(s). +#. Declare the new version in the admin on ``djangoproject.com``. #. Post the blog entry and send out the email announcements. @@ -62,7 +61,7 @@ You'll need a few things hooked up to make this work: * Access to the ``djangoproject.com`` server to upload files and trigger a deploy. -* Access to the admin on ``djangoproject.com``. +* Access to the admin on ``djangoproject.com`` as a "Site maintainer". * Access to post to ``django-announce``. @@ -104,31 +103,15 @@ any time leading up to the actual release: Preparing for release ===================== -Next, everything needs to be made ready for actually rolling the -release. The following things should be done a few days to a few hours -before release: -#. Update the djangoproject home page and download page templates to - reflect the new release. There are two templates to change: - ``flatpages/download.html`` and ``homepage.html``; here's - `one example commit for the 1.4.5 / 1.3.7 releases`__ +Write the announcement blog post for the release. You can enter it into the +admin at any time and mark it as inactive. Here are a few examples: `example +security release announcement`__, `example regular release announcement`__, +`example pre-release announcement`__. - __ https://github.com/django/djangoproject.com/commit/772edbc6ac5a2b8e718606b3338f2bcc429fb9b6 - -#. Write the announcement blog post for the release. You can enter it into - the admin at any time and mark it as inactive. Here are a few examples: - `example security release announcement`__, `example regular release - announcement`__, `example pre-release announcement`__. - - __ https://www.djangoproject.com/weblog/2013/feb/19/security/ - __ https://www.djangoproject.com/weblog/2012/mar/23/14/ - __ https://www.djangoproject.com/weblog/2012/nov/27/15-beta-1/ - -#. Create redirects in the admin for the new downloads. For each release, - we create two redirects that look like:: - - /download//tarball/ -> /m/releases//Django-.tar.gz - /download//checksum/ -> /m/pgp/Django-.checksum.txt +__ https://www.djangoproject.com/weblog/2013/feb/19/security/ +__ https://www.djangoproject.com/weblog/2012/mar/23/14/ +__ https://www.djangoproject.com/weblog/2012/nov/27/15-beta-1/ Actually rolling the release ============================ @@ -144,7 +127,6 @@ OK, this is the fun part, where we actually push out a release! stable/`` (e.g. checkout ``stable/1.5.x`` to issue a release in the 1.5 series) and then ``git pull`` to make sure you're up-to-date. - #. If this is a security release, merge the appropriate patches from ``django-private``. Rebase these patches as necessary to make each one a simple commit on the release branch rather than a merge commit. To ensure @@ -209,7 +191,7 @@ Now you're ready to actually put the release out there. To do this: #. Upload the release package(s) to the djangoproject server; releases go in ``/home/www/djangoproject.com/src/media/releases``, under a directory for the appropriate version number (e.g. - ``/home/www/djangoproject.com/src/media/releases/1.5`` for a ``1.5.X`` + ``/home/www/djangoproject.com/src/media/releases/1.5`` for a ``1.5.x`` release.). #. Upload the checksum file(s); these go in @@ -245,13 +227,10 @@ Now you're ready to actually put the release out there. To do this: work. *FIXME: Is there any reason to pull this file out manually rather than using "python setup.py register"?* -#. Deploy the template changes you made a while back by running `fab deploy` - from the ``djangoproject.com`` repo. +#. Go to the `Add release page in the admin`__, enter the new release number + exactly as it appears in the name of the tarball (Django-.tar.gz). -#. Update the ``/download/`` flat page in the djangoproject.com - admin. For alpha/beta/RC releases, we add a temporary third section - to that page listing the preview package; otherwise, just update - the "Get the latest official version" section. + __ https://www.djangoproject.com/admin/releases/release/add/ #. Make the blog post announcing the release live. @@ -283,7 +262,8 @@ You're almost done! All that's left to do now is: the new version's docs, and update the ``docs/fixtures/doc_releases.json`` JSON fixture. *FIXME: what is the purpose of maintaining this fixture?* -#. Add the release in `Trac's versions list`_. +#. Add the release in `Trac's versions list`_ if necessary. Not all versions + are declared; take example on previous releases. .. _Trac's versions list: https://code.djangoproject.com/admin/ticket/versions -- cgit v1.3 From 3f2befc93163e0666dcc4f745288b98306de4b8e Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Thu, 14 Mar 2013 20:28:24 +0100 Subject: Deprecated django.views.defaults.shortcut. --- django/conf/urls/shortcut.py | 5 +++ django/views/defaults.py | 13 ++++--- docs/internals/deprecation.txt | 3 ++ docs/releases/1.6.txt | 17 +++++++++ docs/topics/http/urls.txt | 1 - tests/contenttypes_tests/__init__.py | 0 tests/contenttypes_tests/fixtures/testdata.json | 47 +++++++++++++++++++++++++ tests/contenttypes_tests/models.py | 24 +++++++++++++ tests/contenttypes_tests/tests.py | 47 +++++++++++++++++++++++++ tests/contenttypes_tests/urls.py | 7 ++++ tests/view_tests/tests/defaults.py | 37 ------------------- tests/view_tests/urls.py | 1 - 12 files changed, 156 insertions(+), 46 deletions(-) create mode 100644 tests/contenttypes_tests/__init__.py create mode 100644 tests/contenttypes_tests/fixtures/testdata.json create mode 100644 tests/contenttypes_tests/models.py create mode 100644 tests/contenttypes_tests/tests.py create mode 100644 tests/contenttypes_tests/urls.py (limited to 'docs/internals') diff --git a/django/conf/urls/shortcut.py b/django/conf/urls/shortcut.py index 6eb2e55e68..c00d176ad6 100644 --- a/django/conf/urls/shortcut.py +++ b/django/conf/urls/shortcut.py @@ -1,5 +1,10 @@ +import warnings + from django.conf.urls import patterns +warnings.warn("django.conf.urls.shortcut will be removed in Django 1.8.", + PendingDeprecationWarning) + urlpatterns = patterns('django.views', (r'^(?P\d+)/(?P.*)/$', 'defaults.shortcut'), ) diff --git a/django/views/defaults.py b/django/views/defaults.py index ec7a233ff7..89228c50c9 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,3 +1,5 @@ +import warnings + from django import http from django.template import (Context, RequestContext, loader, Template, TemplateDoesNotExist) @@ -63,12 +65,9 @@ def permission_denied(request, template_name='403.html'): def shortcut(request, content_type_id, object_id): - # TODO: Remove this in Django 2.0. - # This is a legacy view that depends on the contenttypes framework. - # The core logic was moved to django.contrib.contenttypes.views after - # Django 1.0, but this remains here for backwards compatibility. - # Note that the import is *within* this function, rather than being at - # module level, because we don't want to assume people have contenttypes - # installed. + warnings.warn( + "django.views.defaults.shortcut will be removed in Django 1.8. " + "Import it from django.contrib.contenttypes.views instead.", + PendingDeprecationWarning, stacklevel=2) from django.contrib.contenttypes.views import shortcut as real_shortcut return real_shortcut(request, content_type_id, object_id) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 1305d68859..c0863278b5 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -362,6 +362,9 @@ these changes. * Remove the backward compatible shims introduced to rename the attributes ``ChangeList.root_query_set`` and ``ChangeList.query_set``. +* ``django.conf.urls.shortcut`` and ``django.views.defaults.shortcut`` will be + removed. + * The following private APIs will be removed: - ``django.db.close_connection()`` diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index a44545ddf3..b4c3b70c63 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -352,3 +352,20 @@ private API, it will go through a regular deprecation path. Methods that return a ``QuerySet`` such as ``Manager.get_query_set`` or ``ModelAdmin.queryset`` have been renamed to ``get_queryset``. + +``shortcut`` view and URLconf +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``shortcut`` view was moved from ``django.views.defaults`` to +``django.contrib.contentypes.views`` shortly after the 1.0 release, but the +old location was never deprecated. This oversight was corrected in Django 1.6 +and you should now use the new location. + +The URLconf ``django.conf.urls.shortcut`` was also deprecated. If you're +including it in an URLconf, simply replace:: + + (r'^prefix/', include('django.conf.urls.shortcut')), + +with:: + + (r'^prefix/(?P\d+)/(?P.*)/$', 'django.contrib.contentypes.views'), diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index c5eef8bb41..c1c52f5781 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -330,7 +330,6 @@ itself. It includes a number of other URLconfs:: (r'^comments/', include('django.contrib.comments.urls')), (r'^community/', include('django_website.aggregator.urls')), (r'^contact/', include('django_website.contact.urls')), - (r'^r/', include('django.conf.urls.shortcut')), # ... snip ... ) diff --git a/tests/contenttypes_tests/__init__.py b/tests/contenttypes_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/contenttypes_tests/fixtures/testdata.json b/tests/contenttypes_tests/fixtures/testdata.json new file mode 100644 index 0000000000..52510bfe90 --- /dev/null +++ b/tests/contenttypes_tests/fixtures/testdata.json @@ -0,0 +1,47 @@ +[ + { + "pk": 1, + "model": "contenttypes_tests.author", + "fields": { + "name": "Boris" + } + }, + { + "pk": 1, + "model": "contenttypes_tests.article", + "fields": { + "author": 1, + "title": "Old Article", + "slug": "old_article", + "date_created": "2001-01-01 21:22:23" + } + }, + { + "pk": 2, + "model": "contenttypes_tests.article", + "fields": { + "author": 1, + "title": "Current Article", + "slug": "current_article", + "date_created": "2007-09-17 21:22:23" + } + }, + { + "pk": 3, + "model": "contenttypes_tests.article", + "fields": { + "author": 1, + "title": "Future Article", + "slug": "future_article", + "date_created": "3000-01-01 21:22:23" + } + }, + { + "pk": 1, + "model": "sites.site", + "fields": { + "domain": "testserver", + "name": "testserver" + } + } +] diff --git a/tests/contenttypes_tests/models.py b/tests/contenttypes_tests/models.py new file mode 100644 index 0000000000..3c6685687a --- /dev/null +++ b/tests/contenttypes_tests/models.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import, unicode_literals + +from django.db import models +from django.utils.encoding import python_2_unicode_compatible + +@python_2_unicode_compatible +class Author(models.Model): + name = models.CharField(max_length=100) + + def __str__(self): + return self.name + + def get_absolute_url(self): + return '/views/authors/%s/' % self.id + +@python_2_unicode_compatible +class Article(models.Model): + title = models.CharField(max_length=100) + slug = models.SlugField() + author = models.ForeignKey(Author) + date_created = models.DateTimeField() + + def __str__(self): + return self.title diff --git a/tests/contenttypes_tests/tests.py b/tests/contenttypes_tests/tests.py new file mode 100644 index 0000000000..b39e118ec6 --- /dev/null +++ b/tests/contenttypes_tests/tests.py @@ -0,0 +1,47 @@ +from __future__ import absolute_import, unicode_literals + +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from .models import Author, Article + +class ContentTypesViewsTests(TestCase): + fixtures = ['testdata.json'] + urls = 'contenttypes_tests.urls' + + def test_shortcut_with_absolute_url(self): + "Can view a shortcut for an Author object that has a get_absolute_url method" + for obj in Author.objects.all(): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk) + response = self.client.get(short_url) + self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(), + status_code=302, target_status_code=404) + + def test_shortcut_no_absolute_url(self): + "Shortcuts for an object that has no get_absolute_url method raises 404" + for obj in Article.objects.all(): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk) + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_wrong_type_pk(self): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects') + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_shortcut_bad_pk(self): + short_url = '/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242') + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_nonint_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/shortcut/%s/%s/' % ('spam', an_author.pk) + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) + + def test_bad_content_type(self): + an_author = Author.objects.all()[0] + short_url = '/shortcut/%s/%s/' % (42424242, an_author.pk) + response = self.client.get(short_url) + self.assertEqual(response.status_code, 404) diff --git a/tests/contenttypes_tests/urls.py b/tests/contenttypes_tests/urls.py new file mode 100644 index 0000000000..2cfc90b024 --- /dev/null +++ b/tests/contenttypes_tests/urls.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, unicode_literals + +from django.conf.urls import patterns + +urlpatterns = patterns('', + (r'^shortcut/(\d+)/(.*)/$', 'django.contrib.contenttypes.views.shortcut'), +) diff --git a/tests/view_tests/tests/defaults.py b/tests/view_tests/tests/defaults.py index 3ca7f79136..5efd338d34 100644 --- a/tests/view_tests/tests/defaults.py +++ b/tests/view_tests/tests/defaults.py @@ -13,43 +13,6 @@ class DefaultsTests(TestCase): non_existing_urls = ['/views/non_existing_url/', # this is in urls.py '/views/other_non_existing_url/'] # this NOT in urls.py - def test_shortcut_with_absolute_url(self): - "Can view a shortcut for an Author object that has a get_absolute_url method" - for obj in Author.objects.all(): - short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, obj.pk) - response = self.client.get(short_url) - self.assertRedirects(response, 'http://testserver%s' % obj.get_absolute_url(), - status_code=302, target_status_code=404) - - def test_shortcut_no_absolute_url(self): - "Shortcuts for an object that has no get_absolute_url method raises 404" - for obj in Article.objects.all(): - short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Article).id, obj.pk) - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_wrong_type_pk(self): - short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, 'nobody/expects') - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_shortcut_bad_pk(self): - short_url = '/views/shortcut/%s/%s/' % (ContentType.objects.get_for_model(Author).id, '42424242') - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_nonint_content_type(self): - an_author = Author.objects.all()[0] - short_url = '/views/shortcut/%s/%s/' % ('spam', an_author.pk) - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - - def test_bad_content_type(self): - an_author = Author.objects.all()[0] - short_url = '/views/shortcut/%s/%s/' % (42424242, an_author.pk) - response = self.client.get(short_url) - self.assertEqual(response.status_code, 404) - def test_page_not_found(self): "A 404 status is returned by the page_not_found view" for url in self.non_existing_urls: diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 8a3b492cec..52e2eb474e 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -42,7 +42,6 @@ urlpatterns = patterns('', (r'^$', views.index_page), # Default views - (r'^shortcut/(\d+)/(.*)/$', 'django.views.defaults.shortcut'), (r'^non_existing_url/', 'django.views.defaults.page_not_found'), (r'^server_error/', 'django.views.defaults.server_error'), -- cgit v1.3 From 93cffc3b37d7ef7a20c53f9001f0451a37be7584 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Fri, 22 Mar 2013 05:50:45 -0400 Subject: Added missing markup to docs. --- docs/howto/custom-model-fields.txt | 6 +-- docs/internals/deprecation.txt | 2 +- docs/intro/tutorial03.txt | 4 +- docs/ref/class-based-views/mixins-date-based.txt | 16 +++---- .../ref/class-based-views/mixins-single-object.txt | 2 +- docs/ref/clickjacking.txt | 28 +++++------ docs/ref/contrib/admin/actions.txt | 2 +- docs/ref/contrib/csrf.txt | 2 +- docs/ref/contrib/gis/gdal.txt | 4 +- docs/ref/contrib/sitemaps.txt | 4 +- docs/ref/django-admin.txt | 2 +- docs/ref/models/fields.txt | 4 +- docs/ref/settings.txt | 2 +- docs/ref/template-response.txt | 2 +- docs/ref/urls.txt | 6 +-- docs/releases/1.2-alpha-1.txt | 2 +- docs/releases/1.2.txt | 8 ++-- docs/releases/1.3-alpha-1.txt | 2 +- docs/releases/1.3-beta-1.txt | 6 +-- docs/releases/1.3.txt | 2 +- docs/releases/1.4-alpha-1.txt | 2 +- docs/releases/1.4-beta-1.txt | 2 +- docs/releases/1.4.txt | 2 +- docs/releases/1.5-alpha-1.txt | 15 +++--- docs/releases/1.5-beta-1.txt | 15 +++--- docs/releases/1.5.txt | 21 ++++---- docs/topics/auth/customizing.txt | 56 +++++++++++----------- docs/topics/auth/passwords.txt | 6 +-- docs/topics/class-based-views/mixins.txt | 2 +- docs/topics/db/aggregation.txt | 36 +++++++------- docs/topics/db/managers.txt | 5 +- docs/topics/db/optimization.txt | 2 +- docs/topics/db/queries.txt | 4 +- docs/topics/http/shortcuts.txt | 5 +- docs/topics/logging.txt | 12 ++--- docs/topics/serialization.txt | 24 +++++----- docs/topics/testing/overview.txt | 10 ++-- 37 files changed, 170 insertions(+), 155 deletions(-) (limited to 'docs/internals') diff --git a/docs/howto/custom-model-fields.txt b/docs/howto/custom-model-fields.txt index 84b3881fad..8993872cff 100644 --- a/docs/howto/custom-model-fields.txt +++ b/docs/howto/custom-model-fields.txt @@ -222,9 +222,9 @@ parameters: * :attr:`~django.db.models.Field.db_tablespace`: Only for index creation, if the backend supports :doc:`tablespaces `. You can usually ignore this option. -* ``auto_created``: True if the field was - automatically created, as for the `OneToOneField` used by model - inheritance. For advanced use only. +* ``auto_created``: ``True`` if the field was automatically created, as for the + :class:`~django.db.models.OneToOneField` used by model inheritance. For + advanced use only. All of the options without an explanation in the above list have the same meaning they do for normal Django fields. See the :doc:`field documentation diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index c0863278b5..bf1a323489 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -111,7 +111,7 @@ See the :doc:`Django 1.3 release notes` for more details on these changes. * Starting Django without a :setting:`SECRET_KEY` will result in an exception - rather than a `DeprecationWarning`. (This is accelerated from the usual + rather than a ``DeprecationWarning``. (This is accelerated from the usual deprecation path; see the :doc:`Django 1.4 release notes`.) * The ``mod_python`` request handler will be removed. The ``mod_wsgi`` diff --git a/docs/intro/tutorial03.txt b/docs/intro/tutorial03.txt index daab8b7756..86cc5f97e6 100644 --- a/docs/intro/tutorial03.txt +++ b/docs/intro/tutorial03.txt @@ -107,7 +107,7 @@ with:: url(r'^admin/', include(admin.site.urls)), ) -You have now wired an `index` view into the URLconf. Go to +You have now wired an ``index`` view into the URLconf. Go to http://localhost:8000/polls/ in your browser, and you should see the text "*Hello, world. You're at the poll index.*", which you defined in the ``index`` view. @@ -119,7 +119,7 @@ At this point, it's worth reviewing what these arguments are for. :func:`~django.conf.urls.url` argument: regex --------------------------------------------- -The term `regex` is a commonly used short form meaning `regular expression`, +The term "regex" is a commonly used short form meaning "regular expression", which is a syntax for matching patterns in strings, or in this case, url patterns. Django starts at the first regular expression and makes its way down the list, comparing the requested URL against each regular expression until it diff --git a/docs/ref/class-based-views/mixins-date-based.txt b/docs/ref/class-based-views/mixins-date-based.txt index 7ff201e5a2..75f2a77615 100644 --- a/docs/ref/class-based-views/mixins-date-based.txt +++ b/docs/ref/class-based-views/mixins-date-based.txt @@ -35,8 +35,8 @@ YearMixin Tries the following sources, in order: * The value of the :attr:`YearMixin.year` attribute. - * The value of the `year` argument captured in the URL pattern. - * The value of the `year` GET query argument. + * The value of the ``year`` argument captured in the URL pattern. + * The value of the ``year`` ``GET`` query argument. Raises a 404 if no valid year specification can be found. @@ -87,8 +87,8 @@ MonthMixin Tries the following sources, in order: * The value of the :attr:`MonthMixin.month` attribute. - * The value of the `month` argument captured in the URL pattern. - * The value of the `month` GET query argument. + * The value of the ``month`` argument captured in the URL pattern. + * The value of the ``month`` ``GET`` query argument. Raises a 404 if no valid month specification can be found. @@ -139,8 +139,8 @@ DayMixin Tries the following sources, in order: * The value of the :attr:`DayMixin.day` attribute. - * The value of the `day` argument captured in the URL pattern. - * The value of the `day` GET query argument. + * The value of the ``day`` argument captured in the URL pattern. + * The value of the ``day`` ``GET`` query argument. Raises a 404 if no valid day specification can be found. @@ -192,8 +192,8 @@ WeekMixin Tries the following sources, in order: * The value of the :attr:`WeekMixin.week` attribute. - * The value of the `week` argument captured in the URL pattern - * The value of the `week` GET query argument. + * The value of the ``week`` argument captured in the URL pattern + * The value of the ``week`` ``GET`` query argument. Raises a 404 if no valid week specification can be found. diff --git a/docs/ref/class-based-views/mixins-single-object.txt b/docs/ref/class-based-views/mixins-single-object.txt index 299ac56ac6..bbe930d79e 100644 --- a/docs/ref/class-based-views/mixins-single-object.txt +++ b/docs/ref/class-based-views/mixins-single-object.txt @@ -59,7 +59,7 @@ SingleObjectMixin this view will display. By default, :meth:`get_queryset` returns the value of the :attr:`queryset` attribute if it is set, otherwise it constructs a :class:`~django.db.models.query.QuerySet` by calling - the `all()` method on the :attr:`model` attribute's default manager. + the ``all()`` method on the :attr:`model` attribute's default manager. .. method:: get_context_object_name(obj) diff --git a/docs/ref/clickjacking.txt b/docs/ref/clickjacking.txt index 40b42d1ac7..ce27148ad3 100644 --- a/docs/ref/clickjacking.txt +++ b/docs/ref/clickjacking.txt @@ -30,10 +30,10 @@ Preventing clickjacking Modern browsers honor the `X-Frame-Options`_ HTTP header that indicates whether or not a resource is allowed to load within a frame or iframe. If the response -contains the header with a value of SAMEORIGIN then the browser will only load -the resource in a frame if the request originated from the same site. If the -header is set to DENY then the browser will block the resource from loading in a -frame no matter which site made the request. +contains the header with a value of ``SAMEORIGIN`` then the browser will only +load the resource in a frame if the request originated from the same site. If +the header is set to ``DENY`` then the browser will block the resource from +loading in a frame no matter which site made the request. .. _X-Frame-Options: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header @@ -51,7 +51,7 @@ How to use it Setting X-Frame-Options for all responses ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To set the same X-Frame-Options value for all responses in your site, put +To set the same ``X-Frame-Options`` value for all responses in your site, put ``'django.middleware.clickjacking.XFrameOptionsMiddleware'`` to :setting:`MIDDLEWARE_CLASSES`:: @@ -65,15 +65,15 @@ To set the same X-Frame-Options value for all responses in your site, put This middleware is enabled in the settings file generated by :djadmin:`startproject`. -By default, the middleware will set the X-Frame-Options header to SAMEORIGIN for -every outgoing ``HttpResponse``. If you want DENY instead, set the -:setting:`X_FRAME_OPTIONS` setting:: +By default, the middleware will set the ``X-Frame-Options`` header to +``SAMEORIGIN`` for every outgoing ``HttpResponse``. If you want ``DENY`` +instead, set the :setting:`X_FRAME_OPTIONS` setting:: X_FRAME_OPTIONS = 'DENY' When using the middleware there may be some views where you do **not** want the -X-Frame-Options header set. For those cases, you can use a view decorator that -tells the middleware not to set the header:: +``X-Frame-Options`` header set. For those cases, you can use a view decorator +that tells the middleware not to set the header:: from django.http import HttpResponse from django.views.decorators.clickjacking import xframe_options_exempt @@ -86,7 +86,7 @@ tells the middleware not to set the header:: Setting X-Frame-Options per view ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -To set the X-Frame-Options header on a per view basis, Django provides these +To set the ``X-Frame-Options`` header on a per view basis, Django provides these decorators:: from django.http import HttpResponse @@ -107,8 +107,8 @@ a decorator overrides the middleware. Limitations =========== -The `X-Frame-Options` header will only protect against clickjacking in a modern -browser. Older browsers will quietly ignore the header and need `other +The ``X-Frame-Options`` header will only protect against clickjacking in a +modern browser. Older browsers will quietly ignore the header and need `other clickjacking prevention techniques`_. Browsers that support X-Frame-Options @@ -123,7 +123,7 @@ Browsers that support X-Frame-Options See also ~~~~~~~~ -A `complete list`_ of browsers supporting X-Frame-Options. +A `complete list`_ of browsers supporting ``X-Frame-Options``. .. _complete list: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header#Browser_compatibility .. _other clickjacking prevention techniques: http://en.wikipedia.org/wiki/Clickjacking#Prevention diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index 0a302ecd1d..c79f978850 100644 --- a/docs/ref/contrib/admin/actions.txt +++ b/docs/ref/contrib/admin/actions.txt @@ -175,7 +175,7 @@ That's easy enough to do:: make_published.short_description = "Mark selected stories as published" Notice first that we've moved ``make_published`` into a method and renamed the -`modeladmin` parameter to `self`, and second that we've now put the string +``modeladmin`` parameter to ``self``, and second that we've now put the string ``'make_published'`` in ``actions`` instead of a direct function reference. This tells the :class:`ModelAdmin` to look up the action as a method. diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index 14522d8dbc..968ef0b07b 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -181,7 +181,7 @@ protecting the CSRF token from being sent to other domains. correctly on that version. Make sure you are running at least jQuery 1.5.1. You can use `settings.crossDomain `_ in -jQuery 1.5 and newer in order to replace the `sameOrigin` logic above: +jQuery 1.5 and newer in order to replace the ``sameOrigin`` logic above: .. code-block:: javascript diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt index 161efa39de..c68030673b 100644 --- a/docs/ref/contrib/gis/gdal.txt +++ b/docs/ref/contrib/gis/gdal.txt @@ -634,8 +634,8 @@ systems and coordinate transformation:: or any other input accepted by :class:`SpatialReference` (including spatial reference WKT and PROJ.4 strings, or an integer SRID). By default nothing is returned and the geometry is transformed in-place. - However, if the `clone` keyword is set to ``True`` then a transformed clone - of this geometry is returned instead. + However, if the ``clone`` keyword is set to ``True`` then a transformed + clone of this geometry is returned instead. .. method:: intersects(other) diff --git a/docs/ref/contrib/sitemaps.txt b/docs/ref/contrib/sitemaps.txt index ded7a84fbc..d37ee83378 100644 --- a/docs/ref/contrib/sitemaps.txt +++ b/docs/ref/contrib/sitemaps.txt @@ -454,8 +454,8 @@ cron script, or some other scheduled task. The function makes an HTTP request to Google's servers, so you may not want to introduce that network overhead each time you call ``save()``. -Pinging Google via `manage.py` ------------------------------- +Pinging Google via ``manage.py`` +-------------------------------- .. django-admin:: ping_google diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index ac257db9f5..6277b22a30 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1385,7 +1385,7 @@ For example, to dump data from the database with the alias ``master``:: .. django-admin-option:: --exclude Exclude a specific application from the applications whose contents is -output. For example, to specifically exclude the `auth` application from +output. For example, to specifically exclude the ``auth`` application from the output of dumpdata, you would call:: django-admin.py dumpdata --exclude=auth diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt index 39b84170fe..421de74c62 100644 --- a/docs/ref/models/fields.txt +++ b/docs/ref/models/fields.txt @@ -872,8 +872,8 @@ widget for this field is a :class:`~django.forms.NullBooleanSelect`. .. class:: PositiveIntegerField([**options]) -Like an :class:`IntegerField`, but must be either positive or zero (`0`). -The value `0` is accepted for backward compatibility reasons. +Like an :class:`IntegerField`, but must be either positive or zero (``0``). +The value ``0`` is accepted for backward compatibility reasons. ``PositiveSmallIntegerField`` ----------------------------- diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 2d24ccb441..92500d19d1 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -2174,7 +2174,7 @@ Settings for :mod:`django.contrib.messages`. MESSAGE_LEVEL ------------- -Default: `messages.INFO` +Default: ``messages.INFO`` Sets the minimum message level that will be recorded by the messages framework. See :ref:`message levels ` for more details. diff --git a/docs/ref/template-response.txt b/docs/ref/template-response.txt index 844b5fa46b..5c13ec7d96 100644 --- a/docs/ref/template-response.txt +++ b/docs/ref/template-response.txt @@ -120,7 +120,7 @@ Methods rendered :class:`~django.template.response.SimpleTemplateResponse` instance. - If the callback returns a value that is not `None`, this will be + If the callback returns a value that is not ``None``, this will be used as the response instead of the original response object (and will be passed to the next post rendering callback etc.) diff --git a/docs/ref/urls.txt b/docs/ref/urls.txt index 92b41b8fea..59fb97828c 100644 --- a/docs/ref/urls.txt +++ b/docs/ref/urls.txt @@ -23,12 +23,12 @@ The ``optional_dictionary`` and ``optional_name`` parameters are described in :ref:`Passing extra options to view functions `. .. note:: - Because `patterns()` is a function call, it accepts a maximum of 255 + Because ``patterns()`` is a function call, it accepts a maximum of 255 arguments (URL patterns, in this case). This is a limit for all Python function calls. This is rarely a problem in practice, because you'll - typically structure your URL patterns modularly by using `include()` + typically structure your URL patterns modularly by using ``include()`` sections. However, on the off-chance you do hit the 255-argument limit, - realize that `patterns()` returns a Python list, so you can split up the + realize that ``patterns()`` returns a Python list, so you can split up the construction of the list. :: diff --git a/docs/releases/1.2-alpha-1.txt b/docs/releases/1.2-alpha-1.txt index 16e1940e8f..8c905f6ef0 100644 --- a/docs/releases/1.2-alpha-1.txt +++ b/docs/releases/1.2-alpha-1.txt @@ -428,7 +428,7 @@ Support for multiple databases Django 1.2 adds the ability to use :doc:`more than one database ` in your Django project. Queries can be -issued at a specific database with the `using()` method on +issued at a specific database with the ``using()`` method on querysets; individual objects can be saved to a specific database by providing a ``using`` argument when you save the instance. diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 50c049f5da..ad39062ed1 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -123,9 +123,9 @@ Support for multiple databases Django 1.2 adds the ability to use :doc:`more than one database ` in your Django project. Queries can be issued at a -specific database with the `using()` method on ``QuerySet`` objects. Individual -objects can be saved to a specific database by providing a ``using`` argument -when you call ``save()``. +specific database with the ``using()`` method on ``QuerySet`` objects. +Individual objects can be saved to a specific database by providing a ``using`` +argument when you call ``save()``. Model validation ---------------- @@ -765,7 +765,7 @@ over the next few release cycles. Code taking advantage of any of the features below will raise a ``PendingDeprecationWarning`` in Django 1.2. This warning will be silent by default, but may be turned on using Python's :mod:`warnings` -module, or by running Python with a ``-Wd`` or `-Wall` flag. +module, or by running Python with a ``-Wd`` or ``-Wall`` flag. In Django 1.3, these warnings will become a ``DeprecationWarning``, which is *not* silent. In Django 1.4 support for these features will diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index 53d38a006b..7c9f233921 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -277,7 +277,7 @@ over the next few release cycles. Code taking advantage of any of the features below will raise a ``PendingDeprecationWarning`` in Django 1.3. This warning will be silent by default, but may be turned on using Python's :mod:`warnings` -module, or by running Python with a ``-Wd`` or `-Wall` flag. +module, or by running Python with a ``-Wd`` or ``-Wall`` flag. In Django 1.4, these warnings will become a ``DeprecationWarning``, which is *not* silent. In Django 1.5 support for these features will diff --git a/docs/releases/1.3-beta-1.txt b/docs/releases/1.3-beta-1.txt index 14897ed3b7..69f8023eb3 100644 --- a/docs/releases/1.3-beta-1.txt +++ b/docs/releases/1.3-beta-1.txt @@ -154,7 +154,7 @@ too few. In Django 1.3 we're taking a new approach to this problem, implemented as a pair of changes: -* The choice list for `USStateField` has changed. Previously, it +* The choice list for ``USStateField`` has changed. Previously, it consisted of the 50 U.S. states, the District of Columbia and U.S. overseas territories. As of Django 1.3 it includes all previous choices, plus the U.S. Armed Forces postal codes. @@ -163,7 +163,7 @@ as a pair of changes: ``django.contrib.localflavor.us.models.USPostalCodeField``, has been added which draws its choices from a list of all postal abbreviations recognized by the U.S Postal Service. This includes - all abbreviations recognized by `USStateField`, plus three + all abbreviations recognized by ``USStateField``, plus three independent nations -- the Federated States of Micronesia, the Republic of the Marshall Islands and the Republic of Palau -- which are serviced under treaty by the U.S. postal system. A new form @@ -176,7 +176,7 @@ territories, and other locations serviced by the U.S. postal system. Consult the ``django.contrib.localflavor`` documentation for more details. -The change to `USStateField` is technically backwards-incompatible for +The change to ``USStateField`` is technically backwards-incompatible for users who expect this field to exclude Armed Forces locations. If you need to support U.S. mailing addresses without Armed Forces locations, see the list of choice tuples available in the localflavor diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 582bceffca..f689a3dffd 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -662,7 +662,7 @@ over the next few release cycles. Code taking advantage of any of the features below will raise a ``PendingDeprecationWarning`` in Django 1.3. This warning will be silent by default, but may be turned on using Python's :mod:`warnings` -module, or by running Python with a ``-Wd`` or `-Wall` flag. +module, or by running Python with a ``-Wd`` or ``-Wall`` flag. In Django 1.4, these warnings will become a ``DeprecationWarning``, which is *not* silent. In Django 1.5 support for these features will diff --git a/docs/releases/1.4-alpha-1.txt b/docs/releases/1.4-alpha-1.txt index 09855400eb..92ec0b6483 100644 --- a/docs/releases/1.4-alpha-1.txt +++ b/docs/releases/1.4-alpha-1.txt @@ -878,7 +878,7 @@ removed. The ``open`` method of the base Storage class took an obscure parameter ``mixin`` which allowed you to dynamically change the base classes of the returned file object. This has been removed. In the rare case you relied on the -`mixin` parameter, you can easily achieve the same by overriding the `open` +``mixin`` parameter, you can easily achieve the same by overriding the ``open`` method, e.g.:: from django.core.files import File diff --git a/docs/releases/1.4-beta-1.txt b/docs/releases/1.4-beta-1.txt index 8ea63742e3..d3f1bb807d 100644 --- a/docs/releases/1.4-beta-1.txt +++ b/docs/releases/1.4-beta-1.txt @@ -946,7 +946,7 @@ removed. The ``open`` method of the base Storage class took an obscure parameter ``mixin`` which allowed you to dynamically change the base classes of the returned file object. This has been removed. In the rare case you relied on the -`mixin` parameter, you can easily achieve the same by overriding the `open` +``mixin`` parameter, you can easily achieve the same by overriding the ``open`` method, e.g.:: from django.core.files import File diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 9459e940b4..3025a37098 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -1040,7 +1040,7 @@ removed. The ``open`` method of the base Storage class used to take an obscure parameter ``mixin`` that allowed you to dynamically change the base classes of the returned file object. This has been removed. In the rare case you relied on the -`mixin` parameter, you can easily achieve the same by overriding the `open` +``mixin`` parameter, you can easily achieve the same by overriding the ``open`` method, like this:: from django.core.files import File diff --git a/docs/releases/1.5-alpha-1.txt b/docs/releases/1.5-alpha-1.txt index bb3f32a3be..2588b85306 100644 --- a/docs/releases/1.5-alpha-1.txt +++ b/docs/releases/1.5-alpha-1.txt @@ -227,7 +227,9 @@ GeoDjango :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods (so-called linear referencing). -* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. +* The ``wkb`` and ``hex`` properties of + :class:`~django.contrib.gis.geos.GEOSGeometry` objects preserve the Z + dimension. * Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been dropped. @@ -283,8 +285,8 @@ Django 1.5 also includes several smaller improvements worth noting: * An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on the request as ``resolver_match``. -* By default, all logging messages reaching the `django` logger when - :setting:`DEBUG` is `True` are sent to the console (unless you redefine the +* By default, all logging messages reaching the ``django`` logger when + :setting:`DEBUG` is ``True`` are sent to the console (unless you redefine the logger in your :setting:`LOGGING` setting). * When using :class:`~django.template.RequestContext`, it is now possible to @@ -301,8 +303,9 @@ Django 1.5 also includes several smaller improvements worth noting: whenever a user fails to login successfully. See :data:`~django.contrib.auth.signals.user_login_failed` -* The loaddata management command now supports an `ignorenonexistent` option to - ignore data for fields that no longer exist. +* The loaddata management command now supports an + :djadminopt:`--ignorenonexistent` option to ignore data for fields that no + longer exist. * :meth:`~django.test.SimpleTestCase.assertXMLEqual` and :meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow @@ -556,7 +559,7 @@ Miscellaneous * Uploaded files are no longer created as executable by default. If you need them to be executable change :setting:`FILE_UPLOAD_PERMISSIONS` to your - needs. The new default value is `0666` (octal) and the current umask value + needs. The new default value is ``0666`` (octal) and the current umask value is first masked out. * The :ref:`F() expressions ` supported bitwise operators by diff --git a/docs/releases/1.5-beta-1.txt b/docs/releases/1.5-beta-1.txt index 4dbe77f806..57b13ea6ce 100644 --- a/docs/releases/1.5-beta-1.txt +++ b/docs/releases/1.5-beta-1.txt @@ -225,7 +225,9 @@ GeoDjango :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods (so-called linear referencing). -* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. +* The ``wkb`` and ``hex`` properties of + :class:`~django.contrib.gis.geos.GEOSGeometry` objects preserve the Z + dimension. * Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been dropped. @@ -281,8 +283,8 @@ Django 1.5 also includes several smaller improvements worth noting: * An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on the request as ``resolver_match``. -* By default, all logging messages reaching the `django` logger when - :setting:`DEBUG` is `True` are sent to the console (unless you redefine the +* By default, all logging messages reaching the ``django`` logger when + :setting:`DEBUG` is ``True`` are sent to the console (unless you redefine the logger in your :setting:`LOGGING` setting). * When using :class:`~django.template.RequestContext`, it is now possible to @@ -299,8 +301,9 @@ Django 1.5 also includes several smaller improvements worth noting: whenever a user fails to login successfully. See :data:`~django.contrib.auth.signals.user_login_failed` -* The loaddata management command now supports an `ignorenonexistent` option to - ignore data for fields that no longer exist. +* The loaddata management command now supports an + :djadminopt:`--ignorenonexistent` option to ignore data for fields that no + longer exist. * :meth:`~django.test.SimpleTestCase.assertXMLEqual` and :meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow @@ -595,7 +598,7 @@ Miscellaneous * Uploaded files are no longer created as executable by default. If you need them to be executable change :setting:`FILE_UPLOAD_PERMISSIONS` to your - needs. The new default value is `0666` (octal) and the current umask value + needs. The new default value is ``0666`` (octal) and the current umask value is first masked out. * The :ref:`F() expressions ` supported bitwise operators by diff --git a/docs/releases/1.5.txt b/docs/releases/1.5.txt index 75d2ed0b46..d6ded36a94 100644 --- a/docs/releases/1.5.txt +++ b/docs/releases/1.5.txt @@ -144,9 +144,9 @@ keyword argument ``update_fields``. By using this argument it is possible to save only a select list of model's fields. This can be useful for performance reasons or when trying to avoid overwriting concurrent changes. -Deferred instances (those loaded by .only() or .defer()) will automatically -save just the loaded fields. If any field is set manually after load, that -field will also get updated on save. +Deferred instances (those loaded by ``.only()`` or ``.defer()``) will +automatically save just the loaded fields. If any field is set manually after +load, that field will also get updated on save. See the :meth:`Model.save() ` documentation for more details. @@ -222,7 +222,9 @@ GeoDjango :meth:`~django.contrib.gis.geos.GEOSGeometry.project()` methods (so-called linear referencing). -* The wkb and hex properties of `GEOSGeometry` objects preserve the Z dimension. +* The ``wkb`` and ``hex`` properties of + :class:`~django.contrib.gis.geos.GEOSGeometry` objects preserve the Z + dimension. * Support for PostGIS 2.0 has been added and support for GDAL < 1.5 has been dropped. @@ -292,8 +294,8 @@ Django 1.5 also includes several smaller improvements worth noting: * An instance of :class:`~django.core.urlresolvers.ResolverMatch` is stored on the request as ``resolver_match``. -* By default, all logging messages reaching the `django` logger when - :setting:`DEBUG` is `True` are sent to the console (unless you redefine the +* By default, all logging messages reaching the ``django`` logger when + :setting:`DEBUG` is ``True`` are sent to the console (unless you redefine the logger in your :setting:`LOGGING` setting). * When using :class:`~django.template.RequestContext`, it is now possible to @@ -310,8 +312,9 @@ Django 1.5 also includes several smaller improvements worth noting: whenever a user fails to login successfully. See :data:`~django.contrib.auth.signals.user_login_failed` -* The loaddata management command now supports an `ignorenonexistent` option to - ignore data for fields that no longer exist. +* The loaddata management command now supports an + :djadminopt:`--ignorenonexistent` option to ignore data for fields that no + longer exist. * :meth:`~django.test.SimpleTestCase.assertXMLEqual` and :meth:`~django.test.SimpleTestCase.assertXMLNotEqual` new assertions allow @@ -663,7 +666,7 @@ Miscellaneous * Uploaded files are no longer created as executable by default. If you need them to be executable change :setting:`FILE_UPLOAD_PERMISSIONS` to your - needs. The new default value is `0666` (octal) and the current umask value + needs. The new default value is ``0666`` (octal) and the current umask value is first masked out. * The :ref:`F() expressions ` supported bitwise operators by diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 85124181c6..50ad99c61d 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -103,12 +103,14 @@ the time, it'll just look like this:: class MyBackend(object): def authenticate(self, username=None, password=None): # Check the username/password and return a User. + ... But it could also authenticate a token, like so:: class MyBackend(object): def authenticate(self, token=None): # Check the token and return a User. + ... Either way, ``authenticate`` should check the credentials it gets, and it should return a ``User`` object that matches those credentials, if the @@ -183,9 +185,7 @@ The simple backend above could implement permissions for the magic admin fairly simply:: class SettingsBackend(object): - - # ... - + ... def has_perm(self, user_obj, perm, obj=None): if user_obj.username == settings.ADMIN_LOGIN: return True @@ -482,7 +482,7 @@ Django expects your custom User model to meet some minimum requirements. The easiest way to construct a compliant custom User model is to inherit from :class:`~django.contrib.auth.models.AbstractBaseUser`. :class:`~django.contrib.auth.models.AbstractBaseUser` provides the core -implementation of a `User` model, including hashed passwords and tokenized +implementation of a ``User`` model, including hashed passwords and tokenized password resets. You must then provide some key implementation details: .. currentmodule:: django.contrib.auth @@ -497,7 +497,7 @@ password resets. You must then provide some key implementation details: identifier. The field *must* be unique (i.e., have ``unique=True`` set in it's definition). - In the following example, the field `identifier` is used + In the following example, the field ``identifier`` is used as the identifying field:: class MyUser(AbstractBaseUser): @@ -605,11 +605,11 @@ The following methods are available on any subclass of :meth:`~django.contrib.auth.models.AbstractBaseUser.set_unusable_password()` has been called for this user. -You should also define a custom manager for your User model. If your User -model defines `username` and `email` fields the same as Django's default User, -you can just install Django's -:class:`~django.contrib.auth.models.UserManager`; however, if your User model -defines different fields, you will need to define a custom manager that +You should also define a custom manager for your ``User`` model. If your +``User`` model defines ``username`` and ``email`` fields the same as Django's +default ``User``, you can just install Django's +:class:`~django.contrib.auth.models.UserManager`; however, if your ``User`` +model defines different fields, you will need to define a custom manager that extends :class:`~django.contrib.auth.models.BaseUserManager` providing two additional methods: @@ -617,26 +617,28 @@ additional methods: .. method:: models.CustomUserManager.create_user(*username_field*, password=None, \**other_fields) - The prototype of `create_user()` should accept the username field, + The prototype of ``create_user()`` should accept the username field, plus all required fields as arguments. For example, if your user model - uses `email` as the username field, and has `date_of_birth` as a required - fields, then create_user should be defined as:: + uses ``email`` as the username field, and has ``date_of_birth`` as a + required fields, then ``create_user`` should be defined as:: def create_user(self, email, date_of_birth, password=None): # create user here + ... .. method:: models.CustomUserManager.create_superuser(*username_field*, password, \**other_fields) - The prototype of `create_superuser()` should accept the username field, - plus all required fields as arguments. For example, if your user model - uses `email` as the username field, and has `date_of_birth` as a required - fields, then create_superuser should be defined as:: + The prototype of ``create_superuser()`` should accept the username + field, plus all required fields as arguments. For example, if your user + model uses ``email`` as the username field, and has ``date_of_birth`` + as a required fields, then ``create_superuser`` should be defined as:: def create_superuser(self, email, date_of_birth, password): # create superuser here + ... - Unlike `create_user()`, `create_superuser()` *must* require the caller - to provider a password. + Unlike ``create_user()``, ``create_superuser()`` *must* require the + caller to provider a password. :class:`~django.contrib.auth.models.BaseUserManager` provides the following utility methods: @@ -705,7 +707,7 @@ auth views. * :class:`~django.contrib.auth.forms.PasswordResetForm` Assumes that the user model has an integer primary key, has a field named - `email` that can be used to identify the user, and a boolean field + ``email`` that can be used to identify the user, and a boolean field named `is_active` to prevent password resets for inactive users. * :class:`~django.contrib.auth.forms.SetPasswordForm` @@ -721,8 +723,8 @@ auth views. Works with any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser` -Custom users and django.contrib.admin -------------------------------------- +Custom users and :mod:`django.contrib.admin` +-------------------------------------------- If you want your custom User model to also work with Admin, your User model must define some additional attributes and methods. These methods allow the admin to @@ -732,21 +734,21 @@ control access of the User to admin content: .. attribute:: is_staff - Returns True if the user is allowed to have access to the admin site. + Returns ``True`` if the user is allowed to have access to the admin site. .. attribute:: is_active - Returns True if the user account is currently active. + Returns ``True`` if the user account is currently active. .. method:: has_perm(perm, obj=None): - Returns True if the user has the named permission. If `obj` is + Returns ``True`` if the user has the named permission. If ``obj`` is provided, the permission needs to be checked against a specific object instance. .. method:: has_module_perms(app_label): - Returns True if the user has permission to access models in + Returns ``True`` if the user has permission to access models in the given app. You will also need to register your custom User model with the admin. If @@ -911,7 +913,7 @@ A full example Here is an example of an admin-compliant custom user app. This user model uses an email address as the username, and has a required date of birth; it -provides no permission checking, beyond a simple `admin` flag on the user +provides no permission checking, beyond a simple ``admin`` flag on the user account. This model would be compatible with all the built-in auth forms and views, except for the User creation forms. This example illustrates how most of the components work together, but is not intended to be copied directly into diff --git a/docs/topics/auth/passwords.txt b/docs/topics/auth/passwords.txt index 3d95b4b387..d9b7e24efc 100644 --- a/docs/topics/auth/passwords.txt +++ b/docs/topics/auth/passwords.txt @@ -102,9 +102,9 @@ algorithm. There are several other implementations that allow bcrypt to be used with Django. Django's bcrypt support is NOT directly compatible with these. To upgrade, you will need to modify the - hashes in your database to be in the form `bcrypt$(raw bcrypt - output)`. For example: - `bcrypt$$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy`. + hashes in your database to be in the form ``bcrypt$(raw bcrypt + output)``. For example: + ``bcrypt$$2a$12$NT0I31Sa7ihGEWpka9ASYrEFkhuTNeBQ2xfZskIiiJeyFXhRgS.Sy``. Increasing the work factor -------------------------- diff --git a/docs/topics/class-based-views/mixins.txt b/docs/topics/class-based-views/mixins.txt index 2adbd406c7..9550d2fb86 100644 --- a/docs/topics/class-based-views/mixins.txt +++ b/docs/topics/class-based-views/mixins.txt @@ -13,7 +13,7 @@ but some of it you may want to use separately. For instance, you may want to write a view that renders a template to make the HTTP response, but you can't use :class:`~django.views.generic.base.TemplateView`; perhaps you need to -render a template only on `POST`, with `GET` doing something else +render a template only on ``POST``, with ``GET`` doing something else entirely. While you could use :class:`~django.template.response.TemplateResponse` directly, this will likely result in duplicate code. diff --git a/docs/topics/db/aggregation.txt b/docs/topics/db/aggregation.txt index 49134e24c0..125cd0bdee 100644 --- a/docs/topics/db/aggregation.txt +++ b/docs/topics/db/aggregation.txt @@ -43,7 +43,9 @@ used to track the inventory for a series of online bookstores: Cheat sheet =========== -In a hurry? Here's how to do common aggregate queries, assuming the models above:: +In a hurry? Here's how to do common aggregate queries, assuming the models above: + +.. code-block:: python # Total number of books. >>> Book.objects.count() @@ -140,8 +142,10 @@ will be annotated with the specified values. The syntax for these annotations is identical to that used for the ``aggregate()`` clause. Each argument to ``annotate()`` describes an -aggregate that is to be calculated. For example, to annotate Books with -the number of authors:: +aggregate that is to be calculated. For example, to annotate books with +the number of authors: + +.. code-block:: python # Build an annotated queryset >>> q = Book.objects.annotate(Count('authors')) @@ -190,8 +194,8 @@ you could use the annotation:: >>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price')) -This tells Django to retrieve the Store model, join (through the -many-to-many relationship) with the Book model, and aggregate on the +This tells Django to retrieve the ``Store`` model, join (through the +many-to-many relationship) with the ``Book`` model, and aggregate on the price field of the book model to produce a minimum and maximum value. The same rules apply to the ``aggregate()`` clause. If you wanted to @@ -215,32 +219,32 @@ querying can include traversing "reverse" relationships. The lowercase name of related models and double-underscores are used here too. For example, we can ask for all publishers, annotated with their respective -total book stock counters (note how we use `'book'` to specify the -Publisher->Book reverse foreign key hop):: +total book stock counters (note how we use ``'book'`` to specify the +``Publisher`` -> ``Book`` reverse foreign key hop):: >>> from django.db.models import Count, Min, Sum, Max, Avg >>> Publisher.objects.annotate(Count('book')) -(Every Publisher in the resulting QuerySet will have an extra attribute called -``book__count``.) +(Every ``Publisher`` in the resulting ``QuerySet`` will have an extra attribute +called ``book__count``.) We can also ask for the oldest book of any of those managed by every publisher:: >>> Publisher.objects.aggregate(oldest_pubdate=Min('book__pubdate')) (The resulting dictionary will have a key called ``'oldest_pubdate'``. If no -such alias was specified, it would be the rather long ``'book__pubdate__min'``.) +such alias were specified, it would be the rather long ``'book__pubdate__min'``.) This doesn't apply just to foreign keys. It also works with many-to-many relations. For example, we can ask for every author, annotated with the total number of pages considering all the books he/she has (co-)authored (note how we -use `'book'` to specify the Author->Book reverse many-to-many hop):: +use ``'book'`` to specify the ``Author`` -> ``Book`` reverse many-to-many hop):: >>> Author.objects.annotate(total_pages=Sum('book__pages')) -(Every Author in the resulting QuerySet will have an extra attribute called -``total_pages``. If no such alias was specified, it would be the rather long -``book__pages__sum``.) +(Every ``Author`` in the resulting ``QuerySet`` will have an extra attribute +called ``total_pages``. If no such alias were specified, it would be the rather +long ``book__pages__sum``.) Or ask for the average rating of all the books written by author(s) we have on file:: @@ -248,7 +252,7 @@ file:: >>> Author.objects.aggregate(average_rating=Avg('book__rating')) (The resulting dictionary will have a key called ``'average__rating'``. If no -such alias was specified, it would be the rather long ``'book__rating__avg'``.) +such alias were specified, it would be the rather long ``'book__rating__avg'``.) Aggregations and other QuerySet clauses ======================================= @@ -308,7 +312,7 @@ and the query:: >>> Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count('book')) -Both queries will return a list of Publishers that have at least one good +Both queries will return a list of publishers that have at least one good book (i.e., a book with a rating exceeding 3.0). However, the annotation in the first query will provide the total number of all books published by the publisher; the second query will only include good books in the annotated diff --git a/docs/topics/db/managers.txt b/docs/topics/db/managers.txt index a8c0d17076..dc2f90c8a2 100644 --- a/docs/topics/db/managers.txt +++ b/docs/topics/db/managers.txt @@ -191,7 +191,7 @@ by the default manager. If the normal plain manager class (:class:`django.db.models.Manager`) is not appropriate for your circumstances, you can force Django to use the same class -as the default manager for your model by setting the `use_for_related_fields` +as the default manager for your model by setting the ``use_for_related_fields`` attribute on the manager class. This is documented fully below_. .. _below: manager-types_ @@ -369,7 +369,7 @@ it will use :class:`django.db.models.Manager`. Writing correct Managers for use in automatic Manager instances --------------------------------------------------------------- -As already suggested by the `django.contrib.gis` example, above, the +As already suggested by the :mod:`django.contrib.gis` example, above, the ``use_for_related_fields`` feature is primarily for managers that need to return a custom ``QuerySet`` subclass. In providing this functionality in your manager, there are a couple of things to remember. @@ -413,4 +413,3 @@ used in a model, since the attribute's value is processed when the model class is created and not subsequently reread. Set the attribute on the manager class when it is first defined, as in the initial example of this section and everything will work smoothly. - diff --git a/docs/topics/db/optimization.txt b/docs/topics/db/optimization.txt index b5cca52e23..c1459ae247 100644 --- a/docs/topics/db/optimization.txt +++ b/docs/topics/db/optimization.txt @@ -157,7 +157,7 @@ Doing the following is potentially quite slow: >>> entry = Entry.objects.get(headline__startswith="News") -First of all, `headline` is not indexed, which will make the underlying +First of all, ``headline`` is not indexed, which will make the underlying database fetch slower. Second, the lookup doesn't guarantee that only one object will be returned. diff --git a/docs/topics/db/queries.txt b/docs/topics/db/queries.txt index 91cd4fa871..f19302974d 100644 --- a/docs/topics/db/queries.txt +++ b/docs/topics/db/queries.txt @@ -297,8 +297,8 @@ the query - in this case, it will be a :class:`~django.db.models.query.QuerySet` containing a single element. If you know there is only one object that matches your query, you can use the -:meth:`~django.db.models.query.QuerySet.get` method on a `Manager` which -returns the object directly:: +:meth:`~django.db.models.query.QuerySet.get` method on a +:class:`~django.db.models.Manager` which returns the object directly:: >>> one_entry = Entry.objects.get(pk=1) diff --git a/docs/topics/http/shortcuts.txt b/docs/topics/http/shortcuts.txt index 68860f123f..961f0b9d96 100644 --- a/docs/topics/http/shortcuts.txt +++ b/docs/topics/http/shortcuts.txt @@ -169,8 +169,9 @@ This example is equivalent to:: * A model: the model's `get_absolute_url()` function will be called. - * A view name, possibly with arguments: `urlresolvers.reverse()` will - be used to reverse-resolve the name. + * A view name, possibly with arguments: :func:`urlresolvers.reverse + ` will be used to reverse-resolve the + name. * A URL, which will be used as-is for the redirect location. diff --git a/docs/topics/logging.txt b/docs/topics/logging.txt index 3b68914c1a..cb22a57e84 100644 --- a/docs/topics/logging.txt +++ b/docs/topics/logging.txt @@ -308,7 +308,7 @@ This logging configuration does the following things: * ``simple``, that just outputs the log level name (e.g., ``DEBUG``) and the log message. - The `format` string is a normal Python formatting string + The ``format`` string is a normal Python formatting string describing the details that are to be output on each logging line. The full list of detail that can be output can be found in the `formatter documentation`_. @@ -330,7 +330,7 @@ This logging configuration does the following things: higher) message to ``/dev/null``. * ``console``, a StreamHandler, which will print any ``DEBUG`` - (or higher) message to stderr. This handler uses the `simple` output + (or higher) message to stderr. This handler uses the ``simple`` output format. * ``mail_admins``, an AdminEmailHandler, which will email any @@ -544,7 +544,7 @@ logging module. This filter is used as follows in the default :setting:`LOGGING` configuration to ensure that the :class:`AdminEmailHandler` only sends error - emails to admins when :setting:`DEBUG` is `False`:: + emails to admins when :setting:`DEBUG` is ``False``:: 'filters': { 'require_debug_false': { @@ -564,7 +564,7 @@ logging module. .. versionadded:: 1.5 This filter is similar to :class:`RequireDebugFalse`, except that records are - passed only when :setting:`DEBUG` is `True`. + passed only when :setting:`DEBUG` is ``True``. .. _default-logging-configuration: @@ -576,8 +576,8 @@ with ``ERROR`` or ``CRITICAL`` level are sent to :class:`AdminEmailHandler`, as long as the :setting:`DEBUG` setting is set to ``False``. All messages reaching the ``django`` catch-all logger when :setting:`DEBUG` is -`True` are sent to the console. They are simply discarded (sent to -``NullHandler``) when :setting:`DEBUG` is `False`. +``True`` are sent to the console. They are simply discarded (sent to +``NullHandler``) when :setting:`DEBUG` is ``False``. .. versionchanged:: 1.5 diff --git a/docs/topics/serialization.txt b/docs/topics/serialization.txt index 82cb3ffe5b..ce39f6cd28 100644 --- a/docs/topics/serialization.txt +++ b/docs/topics/serialization.txt @@ -90,11 +90,11 @@ If you only serialize the Restaurant model:: data = serializers.serialize('xml', Restaurant.objects.all()) -the fields on the serialized output will only contain the `serves_hot_dogs` -attribute. The `name` attribute of the base class will be ignored. +the fields on the serialized output will only contain the ``serves_hot_dogs`` +attribute. The ``name`` attribute of the base class will be ignored. -In order to fully serialize your Restaurant instances, you will need to -serialize the Place models as well:: +In order to fully serialize your ``Restaurant`` instances, you will need to +serialize the ``Place`` models as well:: all_objects = list(Restaurant.objects.all()) + list(Place.objects.all()) data = serializers.serialize('xml', all_objects) @@ -176,7 +176,7 @@ XML ~~~ The basic XML serialization format is quite simple:: - + @@ -196,7 +196,7 @@ fields "type" and "name". The text content of the element represents the value that should be stored. Foreign keys and other relational fields are treated a little bit differently:: - + 9 @@ -208,7 +208,7 @@ a foreign key to the contenttypes.ContentType instance with the PK 9. ManyToMany-relations are exported for the model that binds them. For instance, the auth.User model has such a relation to the auth.Permission model:: - + @@ -224,7 +224,7 @@ JSON When staying with the same example data as before it would be serialized as JSON in the following way:: - + [ { "pk": "4b678b301dfd8a4e0dad910de3ae245b", @@ -242,7 +242,7 @@ with three properties: "pk", "model" and "fields". "fields" is again an object containing each field's name and value as property and property-value respectively. -Foreign keys just have the PK of the linked object as property value. +Foreign keys just have the PK of the linked object as property value. ManyToMany-relations are serialized for the model that defines them and are represented as a list of PKs. @@ -273,7 +273,7 @@ YAML YAML serialization looks quite similar to JSON. The object list is serialized as a sequence mappings with the keys "pk", "model" and "fields". Each field is again a mapping with the key being name of the field and the value the value:: - + - fields: {expire_date: !!timestamp '2013-01-16 08:16:59.844560+00:00'} model: sessions.session pk: 4b678b301dfd8a4e0dad910de3ae245b @@ -439,7 +439,7 @@ When ``use_natural_keys=True`` is specified, Django will use the type that defines the method. If you are using :djadmin:`dumpdata` to generate serialized data, you -use the `--natural` command line flag to generate natural keys. +use the :djadminopt:`--natural` command line flag to generate natural keys. .. note:: @@ -458,7 +458,7 @@ Dependencies during serialization Since natural keys rely on database lookups to resolve references, it is important that the data exists before it is referenced. You can't make -a `forward reference` with natural keys -- the data you're referencing +a "forward reference" with natural keys -- the data you're referencing must exist before you include a natural key reference to that data. To accommodate this limitation, calls to :djadmin:`dumpdata` that use diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index cb1c8dc52a..628a161554 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -975,8 +975,8 @@ This class provides some additional capabilities that can be useful for testing Web sites. Converting a normal :class:`unittest.TestCase` to a Django :class:`TestCase` is -easy: Just change the base class of your test from `'unittest.TestCase'` to -`'django.test.TestCase'`. All of the standard Python unit test functionality +easy: Just change the base class of your test from ``'unittest.TestCase'`` to +``'django.test.TestCase'``. All of the standard Python unit test functionality will continue to be available, but it will be augmented with some useful additions, including: @@ -1010,7 +1010,7 @@ This allows the use of automated test clients other than the client, to execute a series of functional tests inside a browser and simulate a real user's actions. -By default the live server's address is `'localhost:8081'` and the full URL +By default the live server's address is ``'localhost:8081'`` and the full URL can be accessed during the tests with ``self.live_server_url``. If you'd like to change the default address (in the case, for example, where the 8081 port is already taken) then you may pass a different one to the :djadmin:`test` command @@ -1117,7 +1117,7 @@ out the `full reference`_ for more details. (for example, just after clicking a link or submitting a form), you might need to check that a response is received by Selenium and that the next page is loaded before proceeding with further test execution. - Do this, for example, by making Selenium wait until the `` HTML tag + Do this, for example, by making Selenium wait until the ```` HTML tag is found in the response (requires Selenium > 2.13): .. code-block:: python @@ -1134,7 +1134,7 @@ out the `full reference`_ for more details. The tricky thing here is that there's really no such thing as a "page load," especially in modern Web apps that generate HTML dynamically after the server generates the initial document. So, simply checking for the presence - of `` in the response might not necessarily be appropriate for all + of ```` in the response might not necessarily be appropriate for all use cases. Please refer to the `Selenium FAQ`_ and `Selenium documentation`_ for more information. -- cgit v1.3 From f02c6c27600c6e78b3f4bb3447cfc025a1f27a90 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sun, 24 Mar 2013 18:31:20 +0100 Subject: Goodbye, Malcolm. --- docs/internals/committers.txt | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs/internals') diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 69c9974967..9b56a40631 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -85,6 +85,8 @@ Malcolm Tredinnick When he's not busy being an International Man of Mystery, Malcolm lives in Sydney, Australia. + *Malcolm passed away on March 17, 2013.* + `Russell Keith-Magee`_ Russell studied physics as an undergraduate, and studied neural networks for his PhD. His first job was with a startup in the defense industry developing -- cgit v1.3 From 6e67d764ae66658b113f99390a80c6f352b74b67 Mon Sep 17 00:00:00 2001 From: Richard Cornish Date: Wed, 27 Mar 2013 00:59:05 -0500 Subject: Updated bios of committers --- docs/internals/committers.txt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/committers.txt b/docs/internals/committers.txt index 9b56a40631..a0649f38a2 100644 --- a/docs/internals/committers.txt +++ b/docs/internals/committers.txt @@ -14,8 +14,9 @@ Journal-World`_ of Lawrence, Kansas, USA. programming", and in technical circles as "the guy who invented Django." He was lead developer at World Online for 2.5 years, during which time - Django was developed and implemented on World Online's sites. He's now the - leader and founder of EveryBlock_, a "news feed for your block". + Django was developed and implemented on World Online's sites. He was the + leader and founder of EveryBlock_, a "news feed for your block." He now + develops for Soundslice_. Adrian lives in Chicago, USA. @@ -40,13 +41,15 @@ Journal-World`_ of Lawrence, Kansas, USA. `Wilson Miner`_ Wilson's design-fu is what makes Django look so nice. He designed the Web site you're looking at right now, as well as Django's acclaimed admin - interface. Wilson is the designer for EveryBlock_. + interface. Wilson was the designer for EveryBlock and Rdio_. He now + designs for Facebook. Wilson lives in San Francisco, USA. .. _lawrence journal-world: http://ljworld.com/ .. _adrian holovaty: http://holovaty.com/ .. _everyblock: http://everyblock.com/ +.. _soundslice: http://www.soundslice.com/ .. _simon willison: http://simonwillison.net/ .. _web-development blog: `simon willison`_ .. _jacob kaplan-moss: http://jacobian.org/ @@ -102,9 +105,9 @@ Malcolm Tredinnick .. _russell keith-magee: http://cecinestpasun.com/ Joseph Kocherhans - Joseph is currently a developer at EveryBlock_, and previously worked for - the Lawrence Journal-World where he built most of the backend for their - Marketplace site. He often disappears for several days into the woods, + Joseph was the director of lead development at EveryBlock and previously + developed at the Lawrence Journal-World. He is treasurer of the `Django + Software Foundation`_. He often disappears for several days into the woods, attempts to teach himself computational linguistics, and annoys his neighbors with his Charango_ playing. @@ -115,6 +118,7 @@ Joseph Kocherhans Joseph lives in Chicago, USA. +.. _django software foundation: https://www.djangoproject.com/foundation/ .. _charango: http://en.wikipedia.org/wiki/Charango `Luke Plant`_ -- cgit v1.3 From e301ea3efb9b59a86c35bc7dcd51b0794212ebdd Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 28 Mar 2013 16:10:11 -0500 Subject: Updated the release document after actually doing a release (!). --- docs/internals/howto-release-django.txt | 99 +++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 36 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index b6f879753a..7f1117abe7 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -53,8 +53,8 @@ Prerequisites You'll need a few things hooked up to make this work: -* A GPG key. *FIXME: sort out exactly whose keys are acceptable for a - release.* +* A GPG key recorded as an acceptable releaser in the `Django releasers`__ + document. * Access to Django's record on PyPI. @@ -68,8 +68,10 @@ You'll need a few things hooked up to make this work: * If this is a security release, access to the pre-notification distribution list. -If this is your first release, you'll need to coordinate with James and Jacob -to get all these things ready to go. +If this is your first release, you'll need to coordinate with James and/or +Jacob to get all these things lined up. + +__ https://www.djangoproject.com/m/pgp/django-releasers.txt Pre-release tasks ================= @@ -103,7 +105,6 @@ any time leading up to the actual release: Preparing for release ===================== - Write the announcement blog post for the release. You can enter it into the admin at any time and mark it as inactive. Here are a few examples: `example security release announcement`__, `example regular release announcement`__, @@ -123,22 +124,30 @@ OK, this is the fun part, where we actually push out a release! __ http://ci.djangoproject.com -#. A release always begins from a release branch, so you should ``git checkout - stable/`` (e.g. checkout ``stable/1.5.x`` to issue a release in the - 1.5 series) and then ``git pull`` to make sure you're up-to-date. +#. A release always begins from a release branch, so you should make sure + you're on a stable branch and up-to-date. For example:: + + git checkout stable/1.5.x + git pull #. If this is a security release, merge the appropriate patches from ``django-private``. Rebase these patches as necessary to make each one a simple commit on the release branch rather than a merge commit. To ensure - this, merge them with the ``--ff-only`` flag; for example, ``git checkout - stable/1.5.x; git merge --ff-only security/1.5.x``, if ``security/1.5.x`` is - a branch in the ``django-private`` repo containing the necessary security - patches for the next release in the 1.5 series. If git refuses to merge with - ``--ff-only``, switch to the security-patch branch and rebase it on the - branch you are about to merge it into (``git checkout security/1.5.x; git - rebase stable/1.5.x``) and then switch back and do the merge. Make sure the - commit message for each security fix explains that the commit is a security - fix and that an announcement will follow (`example security commit`__) + this, merge them with the ``--ff-only`` flag; for example:: + + git checkout stable/1.5.x + git merge --ff-only security/1.5.x + + (this assumes ``security/1.5.x`` is a branch in the ``django-private`` repo + containing the necessary security patches for the next release in the 1.5 + series. + + If git refuses to merge with ``--ff-only``, switch to the security-patch + branch and rebase it on the branch you are about to merge it into (``git + checkout security/1.5.x; git rebase stable/1.5.x``) and then switch back and + do the merge. Make sure the commit message for each security fix explains + that the commit is a security fix and that an announcement will follow + (`example security commit`__) __ https://github.com/django/django/commit/3ef4bbf495cc6c061789132e3d50a8231a89406b @@ -157,20 +166,26 @@ OK, this is the fun part, where we actually push out a release! classifier in ``setup.py`` to reflect this. Otherwise, make sure the classifier is set to ``Development Status :: 5 - Production/Stable``. -#. Tag the release by running ``git tag -s`` *FIXME actual commands*. +#. Tag the release using ``git tag``. For example:: -#. ``git push`` your work. + git tag --sign --message="Django 1.5.1" 1.5.1 + + You can check your work by running ``git tag --verify ``. + +#. Push your work, including the tag: ``git push --tags``. #. Make sure you have an absolutely clean tree by running ``git clean -dfx``. #. Run ``python setup.py sdist`` to generate the release package. This will create the release package in a ``dist/`` directory. -#. Generate the MD5 and SHA1 hashes of the release package:: +#. Generate the hashes of the release package:: $ md5sum dist/Django-.tar.gz $ sha1sum dist/Django-.tar.gz + *FIXME: perhaps we should switch to sha256?* + #. Create a "checksums" file containing the hashes and release information. You can start with `a previous checksums file`__ and replace the dates, keys, links, and checksums. *FIXME: make a template file.* @@ -178,8 +193,9 @@ OK, this is the fun part, where we actually push out a release! __ https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt #. Sign the checksum file using the release key (``gpg - --clearsign``), then verify the signature (``gpg --verify``). *FIXME: - full, actual commands here*. + --clearsign Django-.checksum.txt``). This generates a signed + document, ``Django-.checksum.txt.asc`` which you can then verify + using ``gpg --verify Django-.checksum.txt.asc``. If you're issuing multiple releases, repeat these steps for each release. @@ -201,15 +217,14 @@ Now you're ready to actually put the release out there. To do this: and ``pip``. Here's one method (which requires `virtualenvwrapper`__):: $ mktmpenv - $ easy_install https://www.djangoproject.com/download//tarball/ + $ easy_install https://www.djangoproject.com/m/releases/1.5/Django-1.5.1.tar.gz $ deactivate $ mktmpenv - $ pip install https://www.djangoproject.com/download//tarball/ + $ pip install https://www.djangoproject.com/m/releases/1.5/Django-1.5.1.tar.gz $ deactivate This just tests that the tarballs are available (i.e. redirects are up) and - that they install correctly, but it'll catch silly mistakes. *FIXME: - buildout too?* + that they install correctly, but it'll catch silly mistakes. __ https://pypi.python.org/pypi/virtualenvwrapper @@ -220,15 +235,28 @@ Now you're ready to actually put the release out there. To do this: correct (proper version numbers, no stray ``.pyc`` or other undesirable files). -#. If this is a security or regular release, register the new package with PyPI - by uploading the ``PGK-INFO`` file generated in the release package. This - file's *in* the distribution tarball, so you'll need to pull it out. ``tar - xzf dist/Django-.tar.gz Django-/PKG-INFO`` ought to - work. *FIXME: Is there any reason to pull this file out manually rather than - using "python setup.py register"?* +#. If this is a release that should land on PyPI (i.e. anything except for + a pre-release), register the new package with PyPI by running + ``python setup.py register``. + +#. Upload the sdist you generated a few steps back through the PyPI web + interface. You'll log into PyPI, click "Django" in the right sidebar, + find the release you just registered, and click "files" to upload the + sdist. + + .. note:: + + Why can't we just use ``setup.py sdist upload``? Well, if we do it above + that pushes the sdist to PyPI before we've had a chance to sign, review + and test it. And we can't just ``setup.py upload`` without ``sdist`` + because ``setup.py`` prevents that. Nor can we ``sdist upload`` because + that would generate a *new* sdist that might not match the file we just + signed. Finally, uploading through the web interface is somewhat more + secure: it sends the file over HTTPS. #. Go to the `Add release page in the admin`__, enter the new release number exactly as it appears in the name of the tarball (Django-.tar.gz). + So for example enter "1.5.1" or "1.4-rc-2", etc. __ https://www.djangoproject.com/admin/releases/release/add/ @@ -243,8 +271,7 @@ Now you're ready to actually put the release out there. To do this: #. Post the release announcement to the django-announce, django-developers and django-users mailing lists. This should - include links to both the announcement blog post and the release - notes. *FIXME: make some templates with example text*. + include links to the announcement blog post and the release notes. Post-release ============ @@ -253,8 +280,8 @@ You're almost done! All that's left to do now is: #. Update the ``VERSION`` tuple in ``django/__init__.py`` again, incrementing to whatever the next expected release will be. For - example, after releasing 1.2.1, update ``VERSION`` to report "1.2.2 - pre-alpha". *FIXME: Is this correct? Do we still do this?* + example, after releasing 1.5.1, update ``VERSION`` to + ``VERSION = (1, 5, 2, 'alpha', 0)``. #. For the first alpha release of a new version (when we create the ``stable/1.?.x`` git branch), you'll want to create a new -- cgit v1.3 From d85d393500ae93a40aa6f1251f249e979b91a4d5 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Thu, 28 Mar 2013 15:31:05 -0600 Subject: Minor updates to 'How is Django Formed.' --- docs/internals/howto-release-django.txt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 7f1117abe7..46595956d3 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -54,7 +54,10 @@ Prerequisites You'll need a few things hooked up to make this work: * A GPG key recorded as an acceptable releaser in the `Django releasers`__ - document. + document. (If this key is not your default signing key, you'll need to add + ``-u you@example.com`` to every GPG signing command below, where + ``you@example.com`` is the email address associated with the key you want to + use.) * Access to Django's record on PyPI. @@ -138,9 +141,9 @@ OK, this is the fun part, where we actually push out a release! git checkout stable/1.5.x git merge --ff-only security/1.5.x - (this assumes ``security/1.5.x`` is a branch in the ``django-private`` repo + (This assumes ``security/1.5.x`` is a branch in the ``django-private`` repo containing the necessary security patches for the next release in the 1.5 - series. + series.) If git refuses to merge with ``--ff-only``, switch to the security-patch branch and rebase it on the branch you are about to merge it into (``git @@ -192,10 +195,10 @@ OK, this is the fun part, where we actually push out a release! __ https://www.djangoproject.com/m/pgp/Django-1.5b1.checksum.txt -#. Sign the checksum file using the release key (``gpg - --clearsign Django-.checksum.txt``). This generates a signed - document, ``Django-.checksum.txt.asc`` which you can then verify - using ``gpg --verify Django-.checksum.txt.asc``. +#. Sign the checksum file (``gpg --clearsign + Django-.checksum.txt``). This generates a signed document, + ``Django-.checksum.txt.asc`` which you can then verify using ``gpg + --verify Django-.checksum.txt.asc``. If you're issuing multiple releases, repeat these steps for each release. -- cgit v1.3 From ce23e33399a8a21a87de94bfbf12ae57f833b52c Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Thu, 4 Apr 2013 15:03:45 -0500 Subject: Removed instructions about download_url from release process notes. This is no longer something that has to happen now that 5c771da3 is in. --- docs/internals/howto-release-django.txt | 4 ---- 1 file changed, 4 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/howto-release-django.txt b/docs/internals/howto-release-django.txt index 46595956d3..a49251da76 100644 --- a/docs/internals/howto-release-django.txt +++ b/docs/internals/howto-release-django.txt @@ -161,10 +161,6 @@ OK, this is the fun part, where we actually push out a release! __ https://github.com/django/django/commit/18d920ea4839fb54f9d2a5dcb555b6a5666ee469 - Make sure the ``download_url`` in ``setup.py`` is the actual URL you'll - use for the new release package, not the redirect URL (some tools can't - properly follow redirects). - #. If this is a pre-release package, update the "Development Status" trove classifier in ``setup.py`` to reflect this. Otherwise, make sure the classifier is set to ``Development Status :: 5 - Production/Stable``. -- cgit v1.3 From 4a7292df3be2338ff5d915a9ab3107067f662534 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 8 Apr 2013 19:49:08 +0200 Subject: Removed references to the DDN triage state. Rephrased "How can I help with triaging?" a bit to reflect the current practice. --- docs/internals/_images/triage_process.graffle | 927 +++++------------------ docs/internals/_images/triage_process.pdf | Bin 70123 -> 59051 bytes docs/internals/_images/triage_process.svg | 2 +- docs/internals/contributing/new-contributors.txt | 9 - docs/internals/contributing/triaging-tickets.txt | 58 +- 5 files changed, 230 insertions(+), 766 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/_images/triage_process.graffle b/docs/internals/_images/triage_process.graffle index cd1e89cc3a..291c0397f7 100644 --- a/docs/internals/_images/triage_process.graffle +++ b/docs/internals/_images/triage_process.graffle @@ -14,7 +14,7 @@ BackgroundGraphic Bounds - {{0, 0}, {1118.5799560546875, 782.8900146484375}} + {{0, 0}, {559.28997802734375, 782.8900146484375}} Class SolidGraphic ID @@ -54,19 +54,17 @@ Class LineGraphic + Head + + ID + 132 + ID - 104 - OrthogonalBarAutomatic - - OrthogonalBarPoint - {0, 0} - OrthogonalBarPosition - -1 + 151 Points - {98.499995506345428, 441} - {45, 441} - {36, 576} + {252, 288} + {315, 324} Style @@ -75,173 +73,35 @@ Color b - 0.6 + 0 g - 0.6 + 0.501961 r - 0.6 + 0 HeadArrow - 0 + FilledArrow Legacy - LineType - 2 - Pattern - 1 TailArrow 0 + Width + 2 Tail ID - 103 - - - - Bounds - {{99, 432}, {18, 18}} - Class - ShapedGraphic - ID - 103 - Shape - Circle - Style - - fill - - Draws - NO - - shadow - - Draws - NO - - stroke - - Color - - b - 0.6 - g - 0.6 - r - 0.6 - - Pattern - 1 - - - - - Bounds - {{27, 576}, {342, 36}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 12 - - HFlip - YES - ID - 102 - Shape - Rectangle - Style - - shadow - - Draws - NO - - stroke - - Color - - b - 0.6 - g - 0.6 - r - 0.6 - - Pattern - 1 - - - Text - - Pad - 4 - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 -\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc - -\f0\i\fs24 \cf2 The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is.} - - VFlip - YES - - - Bounds - {{27, 543.5}, {342, 12}} - Class - ShapedGraphic - FitText - Vertical - Flow - Resize - ID - 100 - Shape - Rectangle - Style - - fill - - Draws - NO - - shadow - - Draws - NO - - stroke - - Draws - NO - - - Text - - Pad - 0 - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 -\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc - -\f0\i\fs20 \cf0 For clarity, only the most common transitions are shown.} - VerticalPad - 0 + 82 + Info + 1 Class LineGraphic ID - 98 + 104 OrthogonalBarAutomatic OrthogonalBarPoint @@ -250,9 +110,9 @@ -1 Points - {98.499995506345428, 333} - {45, 333} - {36, 189} + {134.4999955076145, 414} + {90, 414} + {81, 522} Style @@ -282,16 +142,16 @@ Tail ID - 97 + 103 Bounds - {{99, 324}, {18, 18}} + {{135, 405}, {18, 18}} Class ShapedGraphic ID - 97 + 103 Shape Circle Style @@ -324,7 +184,7 @@ Bounds - {{27, 135}, {108, 54}} + {{72, 522}, {342, 36}} Class ShapedGraphic FontInfo @@ -337,7 +197,7 @@ HFlip YES ID - 96 + 102 Shape Rectangle Style @@ -367,63 +227,32 @@ Pad 4 Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;\red102\green102\blue102;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc -\f0\i\fs24 \cf2 The ticket is a bug and obviously should be fixed.} +\f0\i\fs24 \cf2 The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is.} VFlip YES - - Bounds - {{189, 306}, {18, 18}} - Class - ShapedGraphic - ID - 94 - Shape - Circle - Style - - fill - - Draws - NO - - shadow - - Draws - NO - - stroke - - Color - - b - 0.6 - g - 0.6 - r - 0.6 - - Pattern - 1 - - - Class LineGraphic ID - 93 + 98 + OrthogonalBarAutomatic + + OrthogonalBarPoint + {0, 0} + OrthogonalBarPosition + -1 Points - {204.18279336665475, 307.78674107223611} - {252, 252} - {252, 189} + {134.4999955076145, 324} + {90, 324} + {81, 198} Style @@ -442,6 +271,8 @@ 0 Legacy + LineType + 2 Pattern 1 TailArrow @@ -451,71 +282,16 @@ Tail ID - 94 - - - - Bounds - {{162, 135}, {180, 54}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 12 - - HFlip - YES - ID - 95 - Shape - Rectangle - Style - - shadow - - Draws - NO - - stroke - - Color - - b - 0.6 - g - 0.6 - r - 0.6 - - Pattern - 1 - - - Text - - Pad - 4 - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 -\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc - -\f0\i\fs24 \cf2 The ticket requires a discussion by the community and a design decision by a core developer.} + 97 - VFlip - YES Bounds - {{387, 279}, {18, 18}} + {{135, 315}, {18, 18}} Class ShapedGraphic ID - 91 + 97 Shape Circle Style @@ -546,50 +322,9 @@ - - Class - LineGraphic - ID - 90 - Points - - {396, 278.49999548261451} - {396, 189} - - Style - - stroke - - Color - - b - 0.6 - g - 0.6 - r - 0.6 - - HeadArrow - 0 - Legacy - - LineType - 1 - Pattern - 1 - TailArrow - 0 - - - Tail - - ID - 91 - - Bounds - {{369, 135}, {198, 54}} + {{72, 144}, {99, 54}} Class ShapedGraphic FontInfo @@ -602,7 +337,7 @@ HFlip YES ID - 89 + 96 Shape Rectangle Style @@ -632,206 +367,62 @@ Pad 4 Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;\red102\green102\blue102;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc -\f0\i\fs24 \cf2 The ticket was already reported, isn't a bug, doesn't provide enough information, or can't be reproduced.} +\f0\i\fs24 \cf2 The ticket is a bug and should be fixed.} VFlip YES + Bounds + {{243, 279}, {18, 18}} Class - LineGraphic - Head - - ID - 132 - Info - 4 - - ID - 134 - Points - - {342, 342} - {393, 395} - {450, 450} - - Style - - stroke - - Color - - b - 0.501961 - g - 0.25098 - r - 0 - - HeadArrow - FilledArrow - Legacy - - TailArrow - 0 - Width - 2 - - - Tail - - ID - 16 - - - - Class - LineGraphic - Head - - ID - 132 - + ShapedGraphic ID - 133 - Points - - {342, 450} - {450, 450} - + 91 + Shape + Circle Style - stroke + fill - Color - - b - 0.501961 - g - 0.25098 - r - 0 - - HeadArrow - FilledArrow - Legacy - - TailArrow - 0 - Width - 2 + Draws + NO - - Tail - - ID - 17 - - - - Class - LineGraphic - Head - - ID - 10 - - ID - 60 - Points - - {108, 423} - {108, 477} - - Style - - stroke + shadow - Color - - b - 0 - g - 0.501961 - r - 0 - - HeadArrow - FilledArrow - Legacy - - TailArrow - 0 - Width - 2 + Draws + NO - - Tail - - ID - 11 - - - - Class - LineGraphic - ID - 82 - Points - - {162, 288} - {396, 288} - - Style - stroke Color b - 0 + 0.6 g - 0.501961 + 0.6 r - 0 + 0.6 - HeadArrow - 0 - Legacy - - TailArrow - 0 - Width - 2 + Pattern + 1 - Tail - - ID - 12 - Info - 3 - Class LineGraphic - Head - - ID - 11 - ID - 54 + 90 Points - {108, 315} - {108, 369} + {252, 278.49999548068274} + {252, 198} Style @@ -840,75 +431,84 @@ Color b - 0 + 0.6 g - 0.501961 + 0.6 r - 0 + 0.6 HeadArrow - FilledArrow + 0 Legacy + LineType + 1 + Pattern + 1 TailArrow 0 - Width - 2 Tail ID - 12 - Info - 1 + 91 + Bounds + {{189, 144}, {243, 54}} Class - LineGraphic - Head + ShapedGraphic + FontInfo - ID - 130 + Font + Helvetica + Size + 12 + HFlip + YES ID - 131 - Points - - {162, 504} - {450, 504} - + 89 + Shape + Rectangle Style + shadow + + Draws + NO + stroke Color b - 0.501961 + 0.6 g - 0.25098 + 0.6 r - 0 + 0.6 - HeadArrow - FilledArrow - Legacy - - TailArrow - 0 - Width - 2 + Pattern + 1 - Tail + Text - ID - 10 - Info - 3 + Pad + 4 + Text + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 +\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} +{\colortbl;\red255\green255\blue255;\red102\green102\blue102;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc + +\f0\i\fs24 \cf2 The ticket was already reported, was already rejected, isn't a bug, doesn't contain enough information, or can't be reproduced.} + VFlip + YES Class @@ -916,14 +516,14 @@ Head ID - 11 + 10 ID - 58 + 60 Points - {234.0000000000002, 342} - {162, 396} + {144, 396} + {144, 450} Style @@ -932,9 +532,9 @@ Color b - 0.501961 + 0 g - 0.25098 + 0.501961 r 0 @@ -951,23 +551,18 @@ Tail ID - 16 + 11 Class LineGraphic - Head - - ID - 11 - ID - 57 + 82 Points - {234.0000000000002, 450} - {162, 396} + {198, 288} + {252, 288} Style @@ -976,14 +571,14 @@ Color b - 0.501961 + 0 g - 0.25098 + 0.501961 r 0 HeadArrow - FilledArrow + 0 Legacy TailArrow @@ -995,7 +590,9 @@ Tail ID - 17 + 12 + Info + 3 @@ -1004,14 +601,14 @@ Head ID - 17 + 11 ID - 56 + 54 Points - {288, 369} - {288, 423} + {144, 306} + {144, 360} Style @@ -1020,9 +617,9 @@ Color b - 0.501961 + 0 g - 0.25098 + 0.501961 r 0 @@ -1039,7 +636,9 @@ Tail ID - 16 + 12 + Info + 1 @@ -1048,14 +647,14 @@ Head ID - 16 + 130 ID - 55 + 131 Points - {162, 288} - {234.0000000000002, 342} + {198, 468} + {315, 468} Style @@ -1064,9 +663,9 @@ Color b - 0 - g 0.501961 + g + 0.25098 r 0 @@ -1083,7 +682,9 @@ Tail ID - 12 + 10 + Info + 3 @@ -1100,8 +701,8 @@ 136 Points - {396, 288} - {450, 405} + {252, 288} + {315, 432} Style @@ -1141,13 +742,15 @@ ID 137 + Info + 4 ID 138 Points - {396, 288} - {450, 360} + {252, 288} + {315, 396} Style @@ -1187,13 +790,15 @@ ID 139 + Info + 4 ID 140 Points - {396, 288} - {450, 315} + {252, 288} + {315, 360} Style @@ -1240,8 +845,8 @@ 124 Points - {396, 288} - {450, 270} + {252, 288} + {315, 288} Style @@ -1276,7 +881,7 @@ Bounds - {{315, 630}, {125.99999999999999, 18}} + {{270, 576}, {81, 18}} Class ShapedGraphic FontInfo @@ -1318,17 +923,17 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc -\f0\fs24 \cf0 development status} +\f0\i\fs24 \cf0 status} Bounds - {{26.999999999999993, 650}, {108.00000000000001, 14}} + {{72.000000000000057, 596}, {99, 14}} Class ShapedGraphic FitText @@ -1371,7 +976,7 @@ Pad 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;\red0\green64\blue128;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr @@ -1388,8 +993,8 @@ 44 Points - {144, 657} - {180, 657} + {183.59999999999997, 603} + {221.39999999999998, 603} Style @@ -1419,7 +1024,7 @@ Bounds - {{26.999999999999993, 632}, {108.00000000000001, 14}} + {{72.000000000000057, 578}, {99, 14}} Class ShapedGraphic FitText @@ -1462,7 +1067,7 @@ Pad 0 Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;\red0\green128\blue0;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qr @@ -1479,8 +1084,8 @@ 42 Points - {144, 639} - {180, 639} + {183.59999999999997, 585} + {221.39999999999998, 585} Style @@ -1510,7 +1115,7 @@ Bounds - {{315, 648}, {125.99999999999999, 18}} + {{270, 594}, {81, 18}} Class ShapedGraphic FontInfo @@ -1563,7 +1168,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -1573,7 +1178,7 @@ Bounds - {{441, 630}, {125.99999999999999, 18}} + {{351, 576}, {81, 18}} Class ShapedGraphic FontInfo @@ -1626,7 +1231,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -1636,7 +1241,7 @@ Bounds - {{441, 648}, {125.99999999999999, 18}} + {{351, 594}, {81, 18}} Class ShapedGraphic FontInfo @@ -1689,7 +1294,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -1704,8 +1309,8 @@ 36 Points - {423, 234} - {567, 234} + {288, 243} + {432, 243} Style @@ -1727,8 +1332,8 @@ 33 Points - {27, 234} - {369, 234} + {72, 243} + {216, 243} Style @@ -1745,7 +1350,7 @@ Bounds - {{450, 441}, {90.000000000000014, 18}} + {{315, 315}, {90.000000000000014, 18}} Class ShapedGraphic FontInfo @@ -1788,7 +1393,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -1798,7 +1403,7 @@ Bounds - {{450, 396}, {90.000000000000014, 18}} + {{315, 423}, {90.000000000000014, 18}} Class ShapedGraphic FontInfo @@ -1841,7 +1446,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -1851,7 +1456,7 @@ Bounds - {{450, 351}, {90.000000000000014, 18}} + {{315, 387}, {90.000000000000014, 18}} Class ShapedGraphic FontInfo @@ -1894,7 +1499,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -1904,7 +1509,7 @@ Bounds - {{450, 306}, {90.000000000000014, 18}} + {{315, 351}, {90.000000000000014, 18}} Class ShapedGraphic FontInfo @@ -1947,7 +1552,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -1957,7 +1562,7 @@ Bounds - {{450, 495}, {90.000000000000014, 18}} + {{315, 459}, {90.000000000000014, 18}} Class ShapedGraphic FontInfo @@ -2000,7 +1605,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -2010,7 +1615,7 @@ Bounds - {{450, 261}, {90.000000000000014, 18}} + {{315, 279}, {90.000000000000014, 18}} Class ShapedGraphic FontInfo @@ -2053,7 +1658,7 @@ Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -2063,123 +1668,7 @@ Bounds - {{234, 423}, {108, 54}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 12 - - ID - 17 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - - Shape - Rectangle - Style - - fill - - Color - - a - 0.3 - b - 1 - g - 0.501961 - r - 0 - - - stroke - - CornerRadius - 5 - - - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 -\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc - -\f0\fs24 \cf0 Someday\ -/\ -Mabye} - - - - Bounds - {{234, 315}, {108, 54}} - Class - ShapedGraphic - FontInfo - - Font - Helvetica - Size - 12 - - ID - 16 - Magnets - - {0, 1} - {0, -1} - {1, 0} - {-1, 0} - - Shape - Rectangle - Style - - fill - - Color - - a - 0.3 - b - 1 - g - 0.501961 - r - 0 - - - stroke - - CornerRadius - 5 - - - Text - - Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 -\cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} -{\colortbl;\red255\green255\blue255;} -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc - -\f0\fs24 \cf0 Design\ -Decision\ -Needed} - - - - Bounds - {{54, 261}, {108, 54}} + {{90, 270}, {108, 36}} Class ShapedGraphic FontInfo @@ -2225,7 +1714,7 @@ Needed} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -2235,7 +1724,7 @@ Needed} Bounds - {{54, 369}, {108, 54}} + {{90, 360}, {108, 36}} Class ShapedGraphic FontInfo @@ -2281,7 +1770,7 @@ Needed} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -2291,7 +1780,7 @@ Needed} Bounds - {{54, 477}, {108, 54}} + {{90, 450}, {108, 36}} Class ShapedGraphic FontInfo @@ -2337,7 +1826,7 @@ Needed} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -2347,7 +1836,7 @@ Needed} Bounds - {{27, 207}, {342, 351}} + {{72, 216}, {144, 288}} Class ShapedGraphic FontInfo @@ -2366,7 +1855,7 @@ Needed} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -2377,14 +1866,14 @@ Needed} \fs12 \cf0 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc -\fs24 \cf0 triage state} +\i\fs24 \cf0 triage state} TextPlacement 0 Bounds - {{423, 207}, {144, 351}} + {{288, 216}, {144, 288}} Class ShapedGraphic FontInfo @@ -2403,7 +1892,7 @@ Needed} Text Text - {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf340 + {\rtf1\ansi\ansicpg1252\cocoartf1187\cocoasubrtf370 \cocoascreenfonts1{\fonttbl\f0\fswiss\fcharset0 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc @@ -2414,14 +1903,14 @@ Needed} \fs12 \cf0 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\qc -\fs24 \cf0 resolution} +\i\fs24 \cf0 resolution} TextPlacement 0 Bounds - {{315, 630}, {252, 36}} + {{270, 576}, {162, 36}} Class ShapedGraphic FontInfo @@ -2458,7 +1947,7 @@ Needed} Bounds - {{27, 630}, {180, 36}} + {{72, 576}, {162, 36}} Class ShapedGraphic FontInfo @@ -2506,7 +1995,7 @@ Needed} GuidesVisible YES HPages - 2 + 1 ImageCounter 1 KeepToScale @@ -2546,7 +2035,7 @@ Needed} MasterSheets ModificationDate - 2012-12-22 18:00:58 +0000 + 2013-04-08 16:32:00 +0000 Modifier Aymeric Augustin NotesVisible @@ -2636,7 +2125,7 @@ Needed} SidebarWidth 120 VisibleRegion - {{0, 50.450449800270746}, {950.45043820152921, 662.1621536285536}} + {{-195, 118.01801649706192}, {950.4504382015291, 662.16215362855348}} Zoom 1.1100000143051147 ZoomValues diff --git a/docs/internals/_images/triage_process.pdf b/docs/internals/_images/triage_process.pdf index a157fa8960..f731e3e584 100644 Binary files a/docs/internals/_images/triage_process.pdf and b/docs/internals/_images/triage_process.pdf differ diff --git a/docs/internals/_images/triage_process.svg b/docs/internals/_images/triage_process.svg index 363ba41aef..787f5ca647 100644 --- a/docs/internals/_images/triage_process.svg +++ b/docs/internals/_images/triage_process.svg @@ -1,3 +1,3 @@ -2012-12-22 18:00ZCanevas 1Calque 1Closed ticketsresolutionOpen ticketstriage stateReady for CheckinAcceptedUnreviewedDesignDecisionNeededSomeday/Mabyeduplicatefixedinvalidneedsinfoworksformewontfixcompletedstoppedin progressTicket triagers Committersdevelopment statusThe ticket was already reported, isn't a bug, doesn't provide enough information, or can't be reproduced.The ticket requires a discussion by the community and a design decision by a core developer.The ticket is a bug and obviously should be fixed.For clarity, only the most common transitions are shown.The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is. +2013-04-08 16:32ZCanevas 1Calque 1Closed ticketsresolutionOpen ticketstriage stateReady for CheckinAcceptedUnreviewedduplicatefixedinvalidneedsinfoworksformewontfixcompletedstoppedin progressTicket triagers CommittersstatusThe ticket was already reported, was already rejected, isn't a bug, doesn't contain enough information, or can't be reproduced.The ticket is a bug and should be fixed.The ticket has a patch which applies cleanly and includes all needed tests and docs. A core developer can commit it as is. diff --git a/docs/internals/contributing/new-contributors.txt b/docs/internals/contributing/new-contributors.txt index b752503248..0d4ad0cf3f 100644 --- a/docs/internals/contributing/new-contributors.txt +++ b/docs/internals/contributing/new-contributors.txt @@ -140,12 +140,3 @@ FAQ Short answer: No. It's always better to get another set of eyes on a ticket. If you're having trouble getting that second set of eyes, see question 1, above. - -3. **My ticket has been in DDN forever! What should I do?** - - Design Decision Needed requires consensus about the right solution. At the - very least it needs consensus among the core developers, and ideally it has - consensus from the community as well. The best way to accomplish this is to - start a thread on the django-developers mailing list, and for very complex - issues to start a wiki page summarizing the problem and the possible - solutions. diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index 19298c55fb..9c88d6961b 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -51,8 +51,8 @@ attribute easily tells us what and who each ticket is waiting on. Since a picture is worth a thousand words, let's start there: .. image:: /internals/_images/triage_process.* - :height: 564 - :width: 580 + :height: 501 + :width: 400 :alt: Django's ticket triage workflow We've got two roles in this diagram: @@ -128,30 +128,13 @@ Beyond that there are several considerations: and docs, running the test suite with the included patch, and leaving feedback on the ticket. -* **Accepted + Has Patch + (any other flag)** +* **Accepted + Has Patch + Needs ...** This means the ticket has been reviewed, and has been found to need further work. "Needs tests" and "Needs documentation" are self-explanatory. "Patch needs improvement" will generally be accompanied by a comment on the ticket explaining what is needed to improve the code. -Design Decision Needed -~~~~~~~~~~~~~~~~~~~~~~ - -This stage is for issues which may be contentious, may be backwards -incompatible, or otherwise involve high-level design decisions. These issues -should be discussed either in the ticket comments or on `django-developers`_. - -If a ticket has been marked as "DDN", decisions are generally eventually -made by the core committers, however that is not a requirement. See the -:ref:`New contributors' FAQ` for "My ticket has been in -DDN forever! What should I do?" - -This stage will often be used for feature requests. It can also be used for -issues that *might* be bugs, depending on opinion or interpretation. Obvious -bugs (such as crashes, incorrect query results, or non-compliance with a -standard) skip this stage and move straight to "Accepted". - Ready For Checkin ~~~~~~~~~~~~~~~~~ @@ -165,11 +148,13 @@ RFC forever! What should I do?" Someday/Maybe ~~~~~~~~~~~~~ -Generally only used for vague/high-level features or design ideas. These -tickets are uncommon and overall less useful since they don't describe +This stage isn't shown on the diagram. It's only used by core developers to +keep track of high-level ideas or long term feature requests. + +These tickets are uncommon and overall less useful since they don't describe concrete actionable issues. They are enhancement requests that we might consider adding someday to the framework if an excellent patch is submitted. -These tickets are not a high priority. +They are not a high priority. Other triage attributes ----------------------- @@ -301,20 +286,23 @@ developers and bring the issue to django-developers_ instead. How can I help with triaging? ----------------------------- -Although the core developers make the big decisions in the ticket triage -process, there's a lot that general community members can do to help the -triage process. Really, **ANYONE** can help. +The triage process is primarily driven by community members. Really, +**ANYONE** can help. -Start by `creating an account on Trac`_. If you have an account but have -forgotten your password, you can reset it using the `password reset page`_. +Core developers may provide feedback on issues they're familiar with, or make +decisions on controversial ones, but they aren't responsible for triaging +tickets in general. + +To get involved, start by `creating an account on Trac`_. If you have an +account but have forgotten your password, you can reset it using the `password +reset page`_. Then, you can help out by: * Closing "Unreviewed" tickets as "invalid", "worksforme" or "duplicate." -* Promoting "Unreviewed" tickets to "Design decision needed" if a design - decision needs to be made, or "Accepted" in case of obvious bugs or - sensible, clearly defined, feature requests. +* Closing "Unreviewed" tickets as "needsinfo" when they're feature requests + requiring a discussion on `django-developers`_. * Correcting the "Needs tests", "Needs documentation", or "Has patch" flags for tickets where they are incorrectly set. @@ -322,22 +310,18 @@ Then, you can help out by: * Setting the "`Easy pickings`_" flag for tickets that are small and relatively straightforward. +* Set the *type* of tickets that are still uncategorized. + * Checking that old tickets are still valid. If a ticket hasn't seen any activity in a long time, it's possible that the problem has been fixed but the ticket hasn't yet been closed. -* Contacting the owners of tickets that have been claimed but have not - seen any recent activity. If the owner doesn't respond after a week - or so, remove the owner's claim on the ticket. - * Identifying trends and themes in the tickets. If there a lot of bug reports about a particular part of Django, it may indicate we should consider refactoring that part of the code. If a trend is emerging, you should raise it for discussion (referencing the relevant tickets) on `django-developers`_. -* Set the *type* of tickets that are still uncategorized. - * Verify if patches submitted by other users are correct. If they do and also contain appropriate documentation and tests then move them to the "Ready for Checkin" stage. If they don't then leave a comment to explain -- cgit v1.3 From 18255779e9711354372085179d7bf94d803b6895 Mon Sep 17 00:00:00 2001 From: Preston Holmes Date: Tue, 9 Apr 2013 22:39:36 -0700 Subject: Added some further guidance to "accepted" triage stage Now that DDN is gone, I felt it was worth some extra language about what "accepted" means, and qualify what it means to be "safe" to start writing a patch. --- docs/internals/contributing/triaging-tickets.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index 9c88d6961b..4ad0e8510d 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -119,7 +119,14 @@ Beyond that there are several considerations: * **Accepted + No Flags** The ticket is valid, but no one has submitted a patch for it yet. Often this - means you could safely start writing a patch for it. + means you could safely start writing a patch for it. This is generally more + true for the case of accepted bugs than accepted features. A ticket for a bug + that has been accepted means that the issue has been verified by at least one + triager as a legitimate bug - and should probably be fixed if possible. An + accepted new feature may only mean that one triager thought the feature would + be good to have, but this alone does not represent a consensus view or imply + with any certainty that a patch will be accepted for that feature. Seek more + feedback before writing an extensive patch if you are in doubt. * **Accepted + Has Patch** -- cgit v1.3 From 68d6c52ed63d2b3f4834cd3138a487fe5f6c1fc7 Mon Sep 17 00:00:00 2001 From: Julien Phalip Date: Wed, 10 Apr 2013 17:11:26 -0700 Subject: Turned the triage attributes to actual sections so they can be more easily linked to in the documentation. --- docs/internals/contributing/triaging-tickets.txt | 81 ++++++++++++++++++------ 1 file changed, 61 insertions(+), 20 deletions(-) (limited to 'docs/internals') diff --git a/docs/internals/contributing/triaging-tickets.txt b/docs/internals/contributing/triaging-tickets.txt index 4ad0e8510d..bc6148ca46 100644 --- a/docs/internals/contributing/triaging-tickets.txt +++ b/docs/internals/contributing/triaging-tickets.txt @@ -168,28 +168,41 @@ Other triage attributes A number of flags, appearing as checkboxes in Trac, can be set on a ticket: -* Has patch - This means the ticket has an associated - :doc:`patch`. These will be reviewed - to see if the patch is "good". +Has patch +~~~~~~~~~ -* Needs documentation: - This flag is used for tickets with patches that need associated - documentation. Complete documentation of features is a prerequisite - before we can check them into the codebase. +This means the ticket has an associated +:doc:`patch`. These will be reviewed +to see if the patch is "good". -* Needs tests - This flags the patch as needing associated unit tests. Again, this - is a required part of a valid patch. +Needs documentation +~~~~~~~~~~~~~~~~~~~ -* Patch needs improvement - This flag means that although the ticket *has* a patch, it's not quite - ready for checkin. This could mean the patch no longer applies - cleanly, there is a flaw in the implementation, or that the code - doesn't meet our standards. +This flag is used for tickets with patches that need associated +documentation. Complete documentation of features is a prerequisite +before we can check them into the codebase. -* Easy pickings - Tickets that would require small, easy, patches. +Needs tests +~~~~~~~~~~~ + +This flags the patch as needing associated unit tests. Again, this +is a required part of a valid patch. + +Patch needs improvement +~~~~~~~~~~~~~~~~~~~~~~~ + +This flag means that although the ticket *has* a patch, it's not quite +ready for checkin. This could mean the patch no longer applies +cleanly, there is a flaw in the implementation, or that the code +doesn't meet our standards. + +Easy pickings +~~~~~~~~~~~~~ + +Tickets that would require small, easy, patches. + +Type +~~~~ Tickets should be categorized by *type* between: @@ -203,19 +216,47 @@ Tickets should be categorized by *type* between: For when nothing is broken but something could be made cleaner, better, faster, stronger. -Tickets should also be classified into *components* indicating which area of +Component +~~~~~~~~~ + +Tickets should be classified into *components* indicating which area of the Django codebase they belong to. This makes tickets better organized and easier to find. +Severity +~~~~~~~~ + The *severity* attribute is used to identify blockers, that is, issues which should get fixed before releasing the next version of Django. Typically those issues are bugs causing regressions from earlier versions or potentially causing severe data losses. This attribute is quite rarely used and the vast majority of tickets have a severity of "Normal". -Finally, it is possible to use the *version* attribute to indicate in which +Version +~~~~~~~ + +It is possible to use the *version* attribute to indicate in which version the reported bug was identified. +UI/UX +~~~~~ + +This flag is used for tickets that relate to User Interface and User +Experiences questions. For example, this flag would be appropriate for +user-facing features in forms or the admin interface. + +Cc +~~ + +You may add your username or email address to this field to be notified when +new contributions are made to the ticket. + +Keywords +~~~~~~~~ + +With this field you may label a ticket with multiple keywords. This can be +useful, for example, to group several tickets of a same theme. + .. _closing-tickets: Closing Tickets -- cgit v1.3 From 9ac4dbd7b53d187ca54f28e247d3a120660938ca Mon Sep 17 00:00:00 2001 From: Baptiste Mispelon Date: Sat, 13 Apr 2013 02:02:28 +0200 Subject: Fixed #4592: Made CheckboxSelectMultiple more like RadioSelect I refactored RadioSelect and CheckboxSelectMultiple to make them inherit from a base class, allowing them to share the behavior of being able to iterate over their subwidgets. Thanks to Matt McClanahan for the initial patch and to Claude Paroz for the review. --- django/forms/widgets.py | 132 +++++++++++++++++++------------- docs/internals/deprecation.txt | 3 + docs/ref/forms/widgets.txt | 3 + tests/forms_tests/tests/test_widgets.py | 28 +++++-- 4 files changed, 107 insertions(+), 59 deletions(-) (limited to 'docs/internals') diff --git a/django/forms/widgets.py b/django/forms/widgets.py index b0e355eeb6..e72ab014b8 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -11,6 +11,7 @@ try: from urllib.parse import urljoin except ImportError: # Python 2 from urlparse import urljoin +import warnings from django.conf import settings from django.forms.util import flatatt, to_current_timezone @@ -585,14 +586,16 @@ class SelectMultiple(Select): @python_2_unicode_compatible -class RadioInput(SubWidget): +class ChoiceInput(SubWidget): """ - An object used by RadioFieldRenderer that represents a single - . + An object used by ChoiceFieldRenderer that represents a single + . """ + input_type = None # Subclasses must define this def __init__(self, name, value, attrs, choice, index): - self.name, self.value = name, value + self.name = name + self.value = value self.attrs = attrs self.choice_value = force_text(choice[0]) self.choice_label = force_text(choice[1]) @@ -609,8 +612,7 @@ class RadioInput(SubWidget): label_for = format_html(' for="{0}_{1}"', self.attrs['id'], self.index) else: label_for = '' - choice_label = force_text(self.choice_label) - return format_html('{1} {2}', label_for, self.tag(), choice_label) + return format_html('{1} {2}', label_for, self.tag(), self.choice_label) def is_checked(self): return self.value == self.choice_value @@ -618,34 +620,69 @@ class RadioInput(SubWidget): def tag(self): if 'id' in self.attrs: self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) - final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value) + final_attrs = dict(self.attrs, type=self.input_type, name=self.name, value=self.choice_value) if self.is_checked(): final_attrs['checked'] = 'checked' return format_html('', flatatt(final_attrs)) + +class RadioChoiceInput(ChoiceInput): + input_type = 'radio' + + def __init__(self, *args, **kwargs): + super(RadioChoiceInput, self).__init__(*args, **kwargs) + self.value = force_text(self.value) + + +class RadioInput(RadioChoiceInput): + def __init__(self, *args, **kwargs): + msg = "RadioInput has been deprecated. Use RadioChoiceInput instead." + warnings.warn(msg, PendingDeprecationWarning, stacklevel=2) + super(RadioInput, self).__init__(*args, **kwargs) + + +class CheckboxChoiceInput(ChoiceInput): + input_type = 'checkbox' + + def __init__(self, *args, **kwargs): + super(CheckboxChoiceInput, self).__init__(*args, **kwargs) + self.value = set(force_text(v) for v in self.value) + + def is_checked(self): + return self.choice_value in self.value + + @python_2_unicode_compatible -class RadioFieldRenderer(object): +class ChoiceFieldRenderer(object): """ An object used by RadioSelect to enable customization of radio widgets. """ + choice_input_class = None + def __init__(self, name, value, attrs, choices): - self.name, self.value, self.attrs = name, value, attrs + self.name = name + self.value = value + self.attrs = attrs self.choices = choices def __iter__(self): for i, choice in enumerate(self.choices): - yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i) + yield self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, i) def __getitem__(self, idx): choice = self.choices[idx] # Let the IndexError propogate - return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx) + return self.choice_input_class(self.name, self.value, self.attrs.copy(), choice, idx) def __str__(self): return self.render() def render(self): - """Outputs a
    for this set of radio fields.""" + """ + Outputs a
      for this set of choice fields. + If an id was given to the field, it is applied to the
        (each + item in the list will get an id of `$id_$i`). + """ id_ = self.attrs.get('id', None) start_tag = format_html('
          ', id_) if id_ else '
            ' output = [start_tag] @@ -654,15 +691,25 @@ class RadioFieldRenderer(object): output.append('
          ') return mark_safe('\n'.join(output)) -class RadioSelect(Select): - renderer = RadioFieldRenderer + +class RadioFieldRenderer(ChoiceFieldRenderer): + choice_input_class = RadioChoiceInput + + +class CheckboxFieldRenderer(ChoiceFieldRenderer): + choice_input_class = CheckboxChoiceInput + + +class RendererMixin(object): + renderer = None # subclasses must define this + _empty_value = None def __init__(self, *args, **kwargs): # Override the default renderer if we were passed one. renderer = kwargs.pop('renderer', None) if renderer: self.renderer = renderer - super(RadioSelect, self).__init__(*args, **kwargs) + super(RendererMixin, self).__init__(*args, **kwargs) def subwidgets(self, name, value, attrs=None, choices=()): for widget in self.get_renderer(name, value, attrs, choices): @@ -670,56 +717,35 @@ class RadioSelect(Select): def get_renderer(self, name, value, attrs=None, choices=()): """Returns an instance of the renderer.""" - if value is None: value = '' - str_value = force_text(value) # Normalize to string. + if value is None: + value = self._empty_value final_attrs = self.build_attrs(attrs) choices = list(chain(self.choices, choices)) - return self.renderer(name, str_value, final_attrs, choices) + return self.renderer(name, value, final_attrs, choices) def render(self, name, value, attrs=None, choices=()): return self.get_renderer(name, value, attrs, choices).render() def id_for_label(self, id_): - # RadioSelect is represented by multiple fields, - # each of which has a distinct ID. The IDs are made distinct by a "_X" - # suffix, where X is the zero-based index of the radio field. Thus, - # the label for a RadioSelect should reference the first one ('_0'). + # Widgets using this RendererMixin are made of a collection of + # subwidgets, each with their own