├── .gitignore ├── .gitmodules ├── .nojekyll ├── CNAME ├── LICENSE ├── Makefile ├── README.rst ├── _static ├── building.jpg ├── custom.css ├── diving.jpg ├── eb_logo.gif ├── eb_logo_bw.png └── tutorial │ ├── TemplateDoesNotExist.png │ ├── authz-login-pagenotfound.png │ ├── boostrapped.png │ └── confirm_email.png ├── _templates └── layout.html ├── acknowledgments.rst ├── classbasedviews.rst ├── conf.py ├── forms.rst ├── further-reading.rst ├── handouts ├── Effective-Django-OSCON-2013.pdf └── Effective-Django-PyCon-2013.pdf ├── index.rst ├── intro.rst ├── middleware.rst ├── orm.rst ├── requirements.txt ├── scratchpad ├── form-resources.rst └── gettingstarted.rst ├── settings.py ├── testing.rst └── tutorial ├── additional-views.rst ├── authzn.rst ├── before.rst ├── forms.rst ├── getting-started.rst ├── index.rst ├── models.rst ├── related.rst ├── static.rst └── views.rst /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /eggs 3 | /develop-eggs 4 | /parts 5 | /.installed.cfg 6 | /_serekh 7 | /lib/ 8 | /lib64/ 9 | /include/ 10 | .Python 11 | 12 | *.pyc 13 | /pip-selfcheck.json 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src"] 2 | path = src 3 | url = git@github.com:nyergler/effective-django-tutorial.git 4 | [submodule "_build"] 5 | path = _build 6 | url = git@github.com:nyergler/effective-django.git 7 | branch = gh-pages 8 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/.nojekyll -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | www.effectivedjango.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | "Effectivve Django" is licensed under the Creative Commons 2 | Attribution-ShareAlike 4.0 International License. To view a copy of 3 | this license, visit 4 | http://creativecommons.org/licenses/by-sa/4.0/deed.en_US. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Effective Django 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = ./bin/sphinx-build 7 | PAPER = letter 8 | BUILDDIR = _build 9 | BUILDBRANCH = gh-pages 10 | 11 | # Internal variables. 12 | PAPEROPT_a4 = -D latex_paper_size=a4 13 | PAPEROPT_letter = -D latex_paper_size=letter 14 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 15 | # the i18n builder cannot share the environment and doctrees with the others 16 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 17 | 18 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext check-syntax push all 19 | 20 | help: 21 | @echo "Please use \`make ' where is one of" 22 | @echo " html to make standalone HTML files" 23 | @echo " dirhtml to make HTML files named index.html in directories" 24 | @echo " singlehtml to make a single large HTML file" 25 | @echo " pickle to make pickle files" 26 | @echo " json to make JSON files" 27 | @echo " htmlhelp to make HTML files and a HTML help project" 28 | @echo " qthelp to make HTML files and a qthelp project" 29 | @echo " devhelp to make HTML files and a Devhelp project" 30 | @echo " epub to make an epub" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " text to make text files" 34 | @echo " man to make manual pages" 35 | @echo " texinfo to make Texinfo files" 36 | @echo " info to make Texinfo files and run them through makeinfo" 37 | @echo " gettext to make PO message catalogs" 38 | @echo " changes to make an overview of all changed/added/deprecated items" 39 | @echo " linkcheck to check all external links for integrity" 40 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 41 | 42 | clean: 43 | git --git-dir=$(BUILDDIR)/.git co $(BUILDBRANCH) 44 | rm -rf $(BUILDDIR)/* 45 | git --git-dir=$(BUILDDIR)/.git reset --hard $(BUILDBRANCH) 46 | git --git-dir=$(BUILDDIR)/.git checkout $(BUILDBRANCH) 47 | 48 | html: 49 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/ 50 | @echo 51 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/." 52 | 53 | dirhtml: 54 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 55 | @echo 56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 57 | 58 | singlehtml: 59 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 60 | @echo 61 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 62 | 63 | pickle: 64 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 65 | @echo 66 | @echo "Build finished; now you can process the pickle files." 67 | 68 | json: 69 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 70 | @echo 71 | @echo "Build finished; now you can process the JSON files." 72 | 73 | htmlhelp: 74 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 75 | @echo 76 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 77 | ".hhp project file in $(BUILDDIR)/htmlhelp." 78 | 79 | qthelp: 80 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 81 | @echo 82 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 83 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 84 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/EffectiveDjango.qhcp" 85 | @echo "To view the help file:" 86 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/EffectiveDjango.qhc" 87 | 88 | devhelp: 89 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 90 | @echo 91 | @echo "Build finished." 92 | @echo "To view the help file:" 93 | @echo "# mkdir -p $$HOME/.local/share/devhelp/EffectiveDjango" 94 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/EffectiveDjango" 95 | @echo "# devhelp" 96 | 97 | epub: 98 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 99 | @echo 100 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 101 | 102 | latex: 103 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 104 | @echo 105 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 106 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 107 | "(use \`make latexpdf' here to do that automatically)." 108 | 109 | latexpdf: 110 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 111 | @echo "Running LaTeX files through pdflatex..." 112 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 113 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 114 | 115 | text: 116 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 117 | @echo 118 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 119 | 120 | man: 121 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 122 | @echo 123 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 124 | 125 | texinfo: 126 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 127 | @echo 128 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 129 | @echo "Run \`make' in that directory to run these through makeinfo" \ 130 | "(use \`make info' here to do that automatically)." 131 | 132 | info: 133 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 134 | @echo "Running Texinfo files through makeinfo..." 135 | make -C $(BUILDDIR)/texinfo info 136 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 137 | 138 | gettext: 139 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 140 | @echo 141 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 142 | 143 | changes: 144 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 145 | @echo 146 | @echo "The overview file is in $(BUILDDIR)/changes." 147 | 148 | linkcheck: 149 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 150 | @echo 151 | @echo "Link check complete; look for any errors in the above output " \ 152 | "or in $(BUILDDIR)/linkcheck/output.txt." 153 | 154 | doctest: 155 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 156 | @echo "Testing of doctests in the sources finished, look at the " \ 157 | "results in $(BUILDDIR)/doctest/output.txt." 158 | 159 | slides: 160 | $(SPHINXBUILD) -b slides $(ALLSPHINXOPTS) $(BUILDDIR)/slides 161 | @echo "Build finished. The HTML slides are in $(BUILDDIR)/slides." 162 | 163 | html+slides: html slides 164 | 165 | # Support for flymake-mode's flymake-simple-make-init in Emacs 166 | check-syntax: 167 | $(SPHINXBUILD) -n -N -q -b html $(ALLSPHINXOPTS) $(BUILDDIR)/ 168 | $(SPHINXBUILD) -n -N -q -b slides $(ALLSPHINXOPTS) $(BUILDDIR)/slides 169 | 170 | MESSAGE = $(shell git log -1 --pretty=format:"%s (%h)") 171 | 172 | push: 173 | git --git-dir=$(BUILDDIR)/.git checkout $(BUILDBRANCH) 174 | git --git-dir=$(BUILDDIR)/.git add . 175 | git --git-dir=$(BUILDDIR)/.git commit -m '$(MESSAGE)' 176 | git --git-dir=$(BUILDDIR)/.git push origin gh-pages 177 | 178 | all: clean html slides latexpdf epub 179 | 180 | publish: all push 181 | 182 | # Support for flymake-mode's flymake-simple-make-init in Emacs 183 | check-syntax: 184 | $(SPHINXBUILD) -n -N -q -b html $(ALLSPHINXOPTS) $(BUILDDIR)/ 185 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Effective Django 3 | ================== 4 | 5 | This is the repository for the text `Effective Django`_, an ongoing 6 | work in progress by Nathan Yergler. The sample code is maintained in 7 | the `effective-django-tutorial`_ repository. 8 | 9 | *Effective Django* is authored using `ReStructured Text`_ and Sphinx_. 10 | If you're interested in building HTML, PDF, ePub, or other generated 11 | formats, you can do so by: 12 | 13 | #. If you want to build PDF or ePub output, make sure LaTeX is 14 | installed on your machine. If you only care about HTML output, you 15 | can skip this step. 16 | 17 | For Macs, it is recommended you use `MacTeX`_ 18 | 19 | :: 20 | 21 | $ brew install Caskroom/cask/mactex 22 | 23 | If you're building on Ubuntu, you should install the `texlive` and 24 | `texlive-latex-extra` packages. 25 | 26 | :: 27 | 28 | $ sudo apt-get install texlive texlive-latex-extra 29 | 30 | #. Check out this repository:: 31 | 32 | $ git clone --recursive https://github.com/nyergler/effective-django.git 33 | 34 | Note that in order to build *Effective Django*, the sample code 35 | must be cloned into the ``src`` submodule. Using ``--recursive`` 36 | will accomplish that. 37 | 38 | #. Create a virtualenv_ and install the dependencies:: 39 | 40 | $ virtualenv . 41 | $ . bin/activate 42 | $ pip install -r requirements.txt 43 | 44 | #. Run ``make``:: 45 | 46 | $ make all 47 | 48 | The output will be in the ``_build`` sub-directory. 49 | 50 | To only build HTML, specify the target explicitly:: 51 | 52 | $ make html 53 | 54 | Run ``make`` without any parameters for a list of possible targets. 55 | 56 | .. _`Effective Django`: http://effectivedjango.com/ 57 | .. _`effective-django-tutorial`: https://github.com/nyergler/effective-django-tutorial 58 | .. _`ReStructured Text`: http://docutils.sf.net/ 59 | .. _Sphinx: http://sphinx-doc.org/ 60 | .. _`MacTeX`: http://tug.org/mactex/ 61 | .. _virtualenv: http://www.virtualenv.org/ 62 | -------------------------------------------------------------------------------- /_static/building.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/building.jpg -------------------------------------------------------------------------------- /_static/custom.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Inconsolata|Rosario:400,700); 2 | 3 | /* General styling */ 4 | 5 | body { 6 | /* background: #123 !important;*/ 7 | font-family: 'Rosario', sans-serif; 8 | } 9 | 10 | code, .code, pre { 11 | font-family: 'Inconsolata', 'Droid Sans Mono', 'Courier New', monospace; 12 | font-size: 26px; 13 | line-height: 1.125em; 14 | white-space: pre-wrap; 15 | margin-right: -35px; 16 | padding-right: 0; 17 | } 18 | 19 | /* Title Slide */ 20 | 21 | .slides > article.level-1 { 22 | padding-top: 200px; 23 | padding-left: 0px; 24 | padding-right: 0px; 25 | } 26 | 27 | .slides > article > h1 { 28 | color: #222; 29 | font-size:72px; 30 | margin-top: 250px; 31 | background: rgba(255, 255, 255, 0.65); 32 | line-height: 1.5em; 33 | padding-left: 30px; 34 | } 35 | 36 | .slides > article.level-1 > p { 37 | color: #333; 38 | background: rgba(255, 255, 255, 0.65); 39 | text-align: right; 40 | margin-top: 0px; 41 | padding-right: 20px; 42 | padding-left: 60px; 43 | padding-bottom: 0.5em; 44 | } 45 | 46 | /* Section Slide Styling */ 47 | 48 | /* .slides > article.level-2 { */ 49 | /* padding-top: 450px; */ 50 | /* } */ 51 | 52 | /* .slides > article > h2 { */ 53 | /* position: relative; */ 54 | /* bottom: auto; */ 55 | /* } */ 56 | 57 | /* Interior Slide Styling */ 58 | 59 | .slides > article > h3, 60 | .slides > article > h4 { 61 | font-size: 45px; 62 | line-height: 1.5em; 63 | 64 | padding: 0; 65 | margin: 0; 66 | padding-right: 40px; 67 | 68 | font-weight: 600; 69 | 70 | letter-spacing: -1px; 71 | 72 | color: rgb(90,90,90); /* rgb(70, 214, 189); */ 73 | } 74 | 75 | .slides > article > h2 { 76 | border-bottom: 1px #656565 solid; 77 | width: 100%; 78 | color: rgb(15, 15, 15); /* rgb(0, 162, 165); */ 79 | } 80 | 81 | table.context-table { 82 | border: none; 83 | } 84 | 85 | table.context-table td { 86 | border: none; 87 | text-align: center; 88 | padding: 30px 0px; 89 | } 90 | 91 | table.context-table tr.row-odd { 92 | background: #cdcdcd; 93 | } 94 | 95 | article#cc-chooser > h2, 96 | article#event-creation > h2, 97 | article#danger h2 { 98 | display: none; 99 | } -------------------------------------------------------------------------------- /_static/diving.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/diving.jpg -------------------------------------------------------------------------------- /_static/eb_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/eb_logo.gif -------------------------------------------------------------------------------- /_static/eb_logo_bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/eb_logo_bw.png -------------------------------------------------------------------------------- /_static/tutorial/TemplateDoesNotExist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/tutorial/TemplateDoesNotExist.png -------------------------------------------------------------------------------- /_static/tutorial/authz-login-pagenotfound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/tutorial/authz-login-pagenotfound.png -------------------------------------------------------------------------------- /_static/tutorial/boostrapped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/tutorial/boostrapped.png -------------------------------------------------------------------------------- /_static/tutorial/confirm_email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/_static/tutorial/confirm_email.png -------------------------------------------------------------------------------- /_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | 3 | {%- block extrahead %} 4 | {{ super() }} 5 | 10 | {% endblock %} 11 | 12 | {% block footer %} 13 | {{ super() }} 14 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /acknowledgments.rst: -------------------------------------------------------------------------------- 1 | .. slideconf:: 2 | :theme: single-level 3 | :autoslides: False 4 | 5 | Acknowledgments 6 | =============== 7 | 8 | .. slide:: Thanks! 9 | :level: 2 10 | 11 | nathan@yergler.net 12 | 13 | http://effectivedjango.com/ 14 | http://github.com/nyergler/effectivedjango 15 | 16 | It would have been impossible to put *Effective Django* together with 17 | lots and lots of help. Thanks to PyCon_ and PyOhio_ for allowing me to 18 | speak about these topics and develop the material. Thanks to 19 | Eventbrite_ for supporting my work in this area, and generally just 20 | being an awesome place to work. 21 | 22 | *Effective Django* would have been opaque, disjointed, and confusing 23 | without feedback and patience from early reviewers: Tamara Chu, 24 | Brandon L. Golm, Jason Herbst, Philip John James, Galen Krumel, 25 | Allison Lacker, Sanby Lee, Karl Mendes, Nam-Chi Van, Nicole Zuckerman, 26 | and Madeline_. 27 | 28 | Thanks for reading this far. If you have feedback, comments, or 29 | suggestions, please feel free to email them to me: nathan@yergler.net. 30 | Or find me on `identi.ca`_ or Twitter_ as @nyergler. 31 | 32 | .. _PyCon: http://us.pycon.org/2012 33 | .. _PyOhio: http://pyohio.org/ 34 | .. _Eventbrite: http://www.eventbrite.com 35 | .. _`identi.ca`: http://identi.ca/nyergler 36 | .. _Twitter: http://twitter.com/nyergler 37 | .. _Madeline: http://yergler.net/madeline 38 | -------------------------------------------------------------------------------- /classbasedviews.rst: -------------------------------------------------------------------------------- 1 | .. slideconf:: 2 | :theme: single-level 3 | 4 | =================== 5 | Class Based Views 6 | =================== 7 | 8 | Class Based Views 9 | ================= 10 | 11 | * New in Django 1.3 (generic views) 12 | * Allow "composing" a View from pieces 13 | * Intended to allow more flexible reuse 14 | * Base View plus a set of "mixins" provide composable functionality 15 | * Lots of power, lots of [potential] complexity 16 | 17 | Using Class Based Views 18 | ======================= 19 | 20 | * Subclass ``View`` 21 | * Define a method name that matches the HTTP method you're 22 | implementing 23 | 24 | .. testcode:: 25 | 26 | from django.views.generic import View 27 | 28 | class ContactList(View): 29 | 30 | def get(self): 31 | 32 | return HttpResponse("You have no contacts") 33 | 34 | Using a Template 35 | ---------------- 36 | 37 | .. testcode:: 38 | 39 | from django.views.generic import TemplateView 40 | 41 | class ContactList(TemplateView): 42 | 43 | template_name = 'index.html' # or define get_template_names() 44 | 45 | def get_context_data(self, **kwargs): 46 | 47 | context = super(ContactList, self).\ 48 | get_context_data(**kwargs) 49 | context['first_names'] = ['Nathan', 'Richard'] 50 | 51 | return context 52 | 53 | 54 | Configuring URLs 55 | ---------------- 56 | 57 | * Django URLConf needs a callable, not a class 58 | * ``View`` provides as ``as_view`` method 59 | 60 | :: 61 | 62 | urlpatterns = patterns('', 63 | (r'^index/$', ContactList.as_view()), 64 | ) 65 | 66 | * ``kwargs`` passed to ``as_view`` can override properties on the View 67 | class 68 | * Arguments captured in the URL pattern are available as ``.args`` and 69 | ``.kwargs`` inside your class 70 | 71 | 72 | Idiomatic Class Based Views 73 | =========================== 74 | 75 | * Number of mixins can be confusing 76 | * However there are a few common idioms 77 | * Many times you don't wind up defining the HTTP methods directly, 78 | just the things you need 79 | 80 | Template Views 81 | -------------- 82 | 83 | TemplateView 84 | 85 | * ``get_context_data()`` 86 | * ``template_name``, ``get_template_names()`` 87 | * ``response_class`` 88 | * ``render_to_response()`` 89 | 90 | Forms in Views 91 | -------------- 92 | 93 | ProcessFormView 94 | 95 | * ``form_class`` 96 | * ``get_success_url()`` 97 | * ``form_valid(form)`` 98 | * ``form_invalid(form)`` 99 | 100 | Editing Views 101 | ------------- 102 | 103 | CreateView, UpdateView 104 | 105 | * Includes Form processing 106 | 107 | * ``model`` 108 | * ``get_object()`` 109 | 110 | HTTP Methods 111 | ============ 112 | 113 | * The ``http_method_names`` property defines a list of supported 114 | methods 115 | * In Django 1.5 this is:: 116 | 117 | http_method_names = ['get', 'post', 'put', 'delete', 'head', 118 | 'options', 'trace'] 119 | 120 | * If you want to support something like HTTP ``PATCH``, you need to 121 | add it to that list in your View subclass 122 | * Views will look for a class method named for the HTTP method: 123 | ``get()`` is called for ``GET``, etc. 124 | 125 | Writing Composable Views 126 | ======================== 127 | 128 | * Think about the extension points you need 129 | * Call ``super()`` in your methods: this allows others to mix your 130 | View with others 131 | 132 | Example 133 | ------- 134 | 135 | .. testcode:: 136 | 137 | class EventsPageMixin(object): 138 | """View mixin to include the Event in template context.""" 139 | 140 | def get_event(self): 141 | 142 | if not hasattr(self, 'event'): 143 | self.event = get_event() 144 | 145 | return self.event 146 | 147 | def get_context_data(self, **kwargs): 148 | 149 | context = super(EventsPageMixin, self).\ 150 | get_context_data(**kwargs) 151 | 152 | context['event'] = self.get_event() 153 | 154 | return context 155 | 156 | .. notslides:: 157 | 158 | * No actual view logic 159 | * Subclasses ``object``, not ``View`` 160 | * Calls ``super`` on overridden methods 161 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Effective Django documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Mar 4 11:02:15 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import datetime 15 | import os 16 | import sys 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = [ 31 | 'sphinx.ext.doctest', 32 | 'sphinx.ext.todo', 33 | 'sphinxcontrib.blockdiag', 34 | 'hieroglyph', 35 | 'tut.sphinx', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['_templates'] 40 | 41 | # The suffix of source filenames. 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = u'Effective Django' 52 | copyright = u'2012-2013, Nathan Yergler' 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = '0.1' 60 | # The full version, including alpha/beta/rc tags. 61 | release = '0.1' 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | #language = None 66 | 67 | # There are two options for replacing |today|: either, you set today to some 68 | # non-false value, then it is used: 69 | #today = '' 70 | # Else, today_fmt is used as the format for a strftime call. 71 | #today_fmt = '%B %d, %Y' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = [ 76 | 'README.rst', 77 | '_build', 78 | 'scratchpad', 79 | 80 | # sample code sub-module 81 | 'src', 82 | 83 | # Emacs temporary files 84 | '**.#*', 85 | 86 | # Buildout/virtualenv artifacts 87 | 'eggs', 88 | 'develop-eggs', 89 | 'lib', 90 | 'local', 91 | 'parts', 92 | ] 93 | 94 | # The reST default role (used for this markup: `text`) to use for all documents. 95 | #default_role = None 96 | 97 | # If true, '()' will be appended to :func: etc. cross-reference text. 98 | #add_function_parentheses = True 99 | 100 | # If true, the current module name will be prepended to all description 101 | # unit titles (such as .. function::). 102 | #add_module_names = True 103 | 104 | # If true, sectionauthor and moduleauthor directives will be shown in the 105 | # output. They are ignored by default. 106 | #show_authors = False 107 | 108 | # The name of the Pygments (syntax highlighting) style to use. 109 | pygments_style = 'sphinx' 110 | 111 | # A list of ignored prefixes for module index sorting. 112 | #modindex_common_prefix = [] 113 | 114 | # -- Options for HTML 5 Slide output ------------------------------------------- 115 | 116 | slide_theme = 'single-level' 117 | slide_theme_options = {'custom_css':'custom.css'} 118 | 119 | slide_link_html_to_slides = True 120 | slide_link_html_sections_to_slides = True 121 | slide_relative_path = "./slides/" 122 | 123 | slide_link_to_html = True 124 | slide_html_relative_path = "../" 125 | 126 | # -- Options for HTML output --------------------------------------------------- 127 | 128 | # The theme to use for HTML and HTML Help pages. See the documentation for 129 | # a list of builtin themes. 130 | html_theme = 'nature' 131 | 132 | # Theme options are theme-specific and customize the look and feel of a theme 133 | # further. For a list of options available for each theme, see the 134 | # documentation. 135 | #html_theme_options = {} 136 | 137 | # Add any paths that contain custom themes here, relative to this directory. 138 | #html_theme_path = [] 139 | 140 | # The name for this set of Sphinx documents. If None, it defaults to 141 | # " v documentation". 142 | html_title = project 143 | 144 | # A shorter title for the navigation bar. Default is the same as html_title. 145 | # html_short_title = html_title 146 | 147 | # The name of an image file (relative to this directory) to place at the top 148 | # of the sidebar. 149 | #html_logo = None 150 | 151 | # The name of an image file (within the static path) to use as favicon of the 152 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 153 | # pixels large. 154 | #html_favicon = None 155 | 156 | # Add any paths that contain custom static files (such as style sheets) here, 157 | # relative to this directory. They are copied after the builtin static files, 158 | # so a file named "default.css" will overwrite the builtin "default.css". 159 | html_static_path = ['_static'] 160 | 161 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 162 | # using the given strftime format. 163 | #html_last_updated_fmt = '%b %d, %Y' 164 | 165 | # If true, SmartyPants will be used to convert quotes and dashes to 166 | # typographically correct entities. 167 | #html_use_smartypants = True 168 | 169 | # Custom sidebar templates, maps document names to template names. 170 | #html_sidebars = {} 171 | 172 | # Additional templates that should be rendered to pages, maps page names to 173 | # template names. 174 | #html_additional_pages = {} 175 | 176 | # If false, no module index is generated. 177 | #html_domain_indices = True 178 | 179 | # If false, no index is generated. 180 | html_use_index = False 181 | 182 | # If true, the index is split into individual pages for each letter. 183 | #html_split_index = False 184 | 185 | # If true, links to the reST sources are added to the pages. 186 | #html_show_sourcelink = True 187 | 188 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 189 | #html_show_sphinx = True 190 | 191 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 192 | #html_show_copyright = True 193 | 194 | # If true, an OpenSearch description file will be output, and all pages will 195 | # contain a tag referring to it. The value of this option must be the 196 | # base URL from which the finished HTML is served. 197 | #html_use_opensearch = '' 198 | 199 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 200 | #html_file_suffix = None 201 | 202 | html_extra_path = [ 203 | '.nojekyll', 204 | 'CNAME', 205 | ] 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'EffectiveDjangodoc' 209 | 210 | 211 | # -- Options for LaTeX output -------------------------------------------------- 212 | 213 | if 'latex' in sys.argv: 214 | release = '' 215 | latex_elements = { 216 | 'releasename': 'Build %s' % datetime.date.today().strftime('%Y.%m.%d'), 217 | 'date': datetime.date.today().strftime('%d %B %Y'), 218 | # The paper size ('letterpaper' or 'a4paper'). 219 | #'papersize': 'letterpaper', 220 | 221 | # The font size ('10pt', '11pt' or '12pt'). 222 | #'pointsize': '10pt', 223 | 224 | # Additional stuff for the LaTeX preamble. 225 | #'preamble': '', 226 | } 227 | 228 | # Grouping the document tree into LaTeX files. List of tuples 229 | # (source start file, target name, title, author, documentclass [howto/manual]). 230 | latex_documents = [ 231 | ('tutorial/index', 'EffectiveDjango.tex', u'Effective Django', 232 | u'Nathan Yergler', 'manual'), 233 | ] 234 | 235 | # The name of an image file (relative to this directory) to place at the top of 236 | # the title page. 237 | #latex_logo = None 238 | 239 | # For "manual" documents, if this is true, then toplevel headings are parts, 240 | # not chapters. 241 | #latex_use_parts = False 242 | 243 | # If true, show page references after internal links. 244 | #latex_show_pagerefs = False 245 | 246 | # If true, show URL addresses after external links. 247 | latex_show_urls = True 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #latex_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #latex_domain_indices = True 254 | 255 | 256 | # -- Options for manual page output -------------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [ 261 | ('index', 'effectivedjango', u'Effective Django', 262 | [u'Nathan Yergler'], 1) 263 | ] 264 | 265 | # If true, show URL addresses after external links. 266 | #man_show_urls = False 267 | 268 | 269 | # -- Options for Texinfo output ------------------------------------------------ 270 | 271 | # Grouping the document tree into Texinfo files. List of tuples 272 | # (source start file, target name, title, author, 273 | # dir menu entry, description, category) 274 | texinfo_documents = [ 275 | ('index', 'EffectiveDjango', u'Effective Django', 276 | u'Nathan Yergler', 'EffectiveDjango', 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # -- EPub options 290 | 291 | epub_author = 'Nathan Yergler' 292 | 293 | # -- DocTest Configuration ------------------------------------------------------ 294 | 295 | trim_doctest_flags = True 296 | doctest_global_setup = """ 297 | 298 | # setup the Django settings config 299 | import sys, os 300 | sys.path.append(os.getcwd()) 301 | os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' 302 | 303 | # import commonly needed items 304 | import rebar.testing 305 | 306 | # create a Request Factory 307 | from django.test.client import RequestFactory 308 | request_factory = RequestFactory() 309 | 310 | """ 311 | -------------------------------------------------------------------------------- /forms.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Effective Django Forms 3 | ====================== 4 | 5 | Form Basics 6 | =========== 7 | 8 | Forms in Context 9 | ---------------- 10 | 11 | .. Table:: 12 | :class: context-table 13 | 14 | +-------------------------+---------------------------------+ 15 | | **Views** | Convert Request to Response | 16 | +-------------------------+---------------------------------+ 17 | | **Forms** | Convert input to Python objects | 18 | +-------------------------+---------------------------------+ 19 | | **Models** | Data and business logic | 20 | +-------------------------+---------------------------------+ 21 | 22 | .. Why use Forms? 23 | .. -------------- 24 | 25 | .. - Data type coercion 26 | .. - Validation 27 | .. - Consistent HTML output 28 | 29 | Defining Forms 30 | -------------- 31 | 32 | Forms are composed of fields, which have a widget. 33 | 34 | .. testcode:: 35 | 36 | from django.utils.translation import gettext_lazy as _ 37 | from django import forms 38 | 39 | class ContactForm(forms.Form): 40 | 41 | name = forms.CharField(label=_("Your Name"), 42 | max_length=255, 43 | widget=forms.TextInput, 44 | ) 45 | 46 | email = forms.EmailField(label=_("Email address")) 47 | 48 | Instantiating a Form 49 | -------------------- 50 | 51 | Unbound forms don't have data associated with them, but they can 52 | be rendered:: 53 | 54 | form = ContactForm() 55 | 56 | Bound forms have specific data associated, which can be 57 | validated:: 58 | 59 | form = ContactForm(data=request.POST, files=request.FILES) 60 | 61 | Accessing Fields 62 | ---------------- 63 | 64 | Two ways to access fields on a Form instance 65 | 66 | - ``form.fields['name']`` returns the ``Field`` object 67 | - ``form['name']`` returns a ``BoundField`` 68 | - ``BoundField`` wraps a field and value for HTML output 69 | 70 | Initial Data 71 | ------------ 72 | 73 | .. testcode:: 74 | 75 | form = ContactForm( 76 | initial={ 77 | 'name': 'First and Last Name', 78 | }, 79 | ) 80 | 81 | .. doctest:: 82 | 83 | >>> form['name'].value() 84 | 'First and Last Name' 85 | 86 | 87 | Validation 88 | ========== 89 | 90 | Validating the Form 91 | ------------------- 92 | 93 | .. blockdiag:: 94 | 95 | blockdiag { 96 | // Set labels to nodes. 97 | A [label = "Field Validation"]; 98 | C [label = "Form Validation"]; 99 | 100 | A -> C; 101 | } 102 | 103 | - Only bound forms can be validated 104 | - Calling ``form.is_valid()`` triggers validation if needed 105 | - Validated, cleaned data is stored in ``form.cleaned_data`` 106 | - Calling ``form.full_clean()`` performs the full cycle 107 | 108 | Field Validation 109 | ---------------- 110 | 111 | .. blockdiag:: 112 | 113 | blockdiag { 114 | // Set labels to nodes. 115 | A [label = "for each Field"]; 116 | 117 | B [label = "Field.clean"]; 118 | C [label = "Field.to_python"]; 119 | D [label = "Field validators"]; 120 | 121 | F [label = ".clean_fieldname()"]; 122 | 123 | A -> B; 124 | B -> C; 125 | C -> D; 126 | 127 | A -> F; 128 | } 129 | 130 | - Three phases for Fields: To Python, Validation, and Cleaning 131 | - If validation raises an Error, cleaning is skipped 132 | - Validators are callables that can raise a ``ValidationError`` 133 | - Django includes generic ones for some common tasks 134 | - Examples: URL, Min/Max Value, Min/Max Length, URL, Regex, email 135 | 136 | Field Cleaning 137 | -------------- 138 | 139 | - ``.clean_fieldname()`` method is called after validators 140 | - Input has already been converted to Python objects 141 | - Methods can raise ``ValidationErrors`` 142 | - Methods *must* return the cleaned value 143 | 144 | ``.clean_email()`` 145 | ------------------ 146 | 147 | .. testcode:: 148 | 149 | class ContactForm(forms.Form): 150 | name = forms.CharField( 151 | label=_("Name"), 152 | max_length=255, 153 | ) 154 | 155 | email = forms.EmailField( 156 | label=_("Email address"), 157 | ) 158 | 159 | def clean_email(self): 160 | 161 | if (self.cleaned_data.get('email', '') 162 | .endswith('hotmail.com')): 163 | 164 | raise ValidationError("Invalid email address.") 165 | 166 | return self.cleaned_data.get('email', '') 167 | 168 | Form Validation 169 | --------------- 170 | 171 | - ``.clean()`` performs cross-field validation 172 | - Called even if errors were raised by Fields 173 | - *Must* return the cleaned data dictionary 174 | - ``ValidationErrors`` raised by ``.clean()`` will be grouped in 175 | ``form.non_field_errors()`` by default. 176 | 177 | ``.clean()`` example 178 | -------------------- 179 | 180 | .. testcode:: 181 | 182 | class ContactForm(forms.Form): 183 | name = forms.CharField( 184 | label=_("Name"), 185 | max_length=255, 186 | ) 187 | 188 | email = forms.EmailField(label=_("Email address")) 189 | confirm_email = forms.EmailField(label=_("Confirm")) 190 | 191 | def clean(self): 192 | if (self.cleaned_data.get('email') != 193 | self.cleaned_data.get('confirm_email')): 194 | 195 | raise ValidationError("Email addresses do not match.") 196 | 197 | return self.cleaned_data 198 | 199 | Initial != Default Data 200 | ----------------------- 201 | 202 | - Initial data is used as a starting point 203 | - It does not automatically propagate to ``cleaned_data`` 204 | - Defaults for non-required fields should be specified when 205 | accessing the dict:: 206 | 207 | self.cleaned_data.get('name', 'default') 208 | 209 | Passing Extra Information 210 | ------------------------- 211 | 212 | - Sometimes you need extra information in a form 213 | - Pass as a keyword argument, and pop in __init__ 214 | 215 | .. testcode:: 216 | 217 | class MyForm(forms.Form): 218 | def __init__(self, *args, **kwargs): 219 | self._user = kwargs.pop('user') 220 | super(MyForm, self).__init__(*args, **kwargs) 221 | 222 | Tracking Changes 223 | ---------------- 224 | 225 | - Forms use initial data to track changed fields 226 | - ``form.has_changed()`` 227 | - ``form.changed_data`` 228 | - Fields can render a hidden input with the initial value, as well:: 229 | 230 | >>> changed_date = forms.DateField(show_hidden_initial=True) 231 | >>> print form['changed_date'] 232 | '' 233 | 234 | 235 | Testing 236 | ======= 237 | 238 | Testing Forms 239 | ------------- 240 | 241 | - Remember what Forms are for 242 | - Testing strategies 243 | 244 | * Initial states 245 | * Field Validation 246 | * Final state of ``cleaned_data`` 247 | 248 | Unit Tests 249 | ---------- 250 | 251 | .. testcode:: 252 | 253 | import unittest 254 | 255 | class FormTests(unittest.TestCase): 256 | def test_validation(self): 257 | form_data = { 258 | 'name': 'X' * 300, 259 | } 260 | 261 | form = ContactForm(data=form_data) 262 | self.assertFalse(form.is_valid()) 263 | 264 | Test Data 265 | --------- 266 | 267 | .. testcode:: 268 | 269 | from rebar.testing import flatten_to_dict 270 | 271 | form_data = flatten_to_dict(ContactForm()) 272 | form_data.update({ 273 | 'name': 'X' * 300, 274 | }) 275 | form = ContactForm(data=form_data) 276 | assert(not form.is_valid()) 277 | 278 | 279 | Rendering Forms 280 | =============== 281 | 282 | Idiomatic Form Usage 283 | -------------------- 284 | 285 | .. testcode:: 286 | 287 | from django.views.generic.edit import FormMixin, ProcessFormView 288 | 289 | class ContactView(FormMixin, ProcessFormView): 290 | form_class = ContactForm 291 | success_url = '/contact/sent' 292 | 293 | def form_valid(self, form): 294 | # do something -- save, send, etc 295 | pass 296 | 297 | def form_invalid(self, form): 298 | # do something -- log the error, etc -- if needed 299 | pass 300 | 301 | Form Output 302 | ----------- 303 | 304 | Three primary "whole-form" output modes: 305 | 306 | - ``form.as_p()``, ``form.as_ul()``, ``form.as_table()`` 307 | 308 | :: 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | Controlling Form Output 320 | ----------------------- 321 | 322 | :: 323 | 324 | {% for field in form %} 325 | {{ field.label_tag }}: {{ field }} 326 | {{ field.errors }} 327 | {% endfor %} 328 | {{ form.non_field_errors }} 329 | 330 | Additional rendering properties: 331 | 332 | - ``field.label`` 333 | - ``field.label_tag`` 334 | - ``field.html_name`` 335 | - ``field.help_text`` 336 | 337 | Customizing Rendering 338 | --------------------- 339 | 340 | You can specify additional attributes for widgets as part of the form 341 | definition. 342 | 343 | .. testcode:: 344 | 345 | class ContactForm(forms.Form): 346 | name = forms.CharField( 347 | max_length=255, 348 | widget=forms.Textarea( 349 | attrs={'class': 'custom'}, 350 | ), 351 | ) 352 | 353 | You can also specify form-wide CSS classes to add for error and 354 | required states. 355 | 356 | .. testcode:: 357 | 358 | class ContactForm(forms.Form): 359 | error_css_class = 'error' 360 | required_css_class = 'required' 361 | 362 | 363 | Customizing Error Messages 364 | -------------------------- 365 | 366 | Built in validators have default error messages 367 | 368 | .. doctest:: 369 | 370 | >>> generic = forms.CharField() 371 | >>> generic.clean('') 372 | Traceback (most recent call last): 373 | ... 374 | ValidationError: [u'This field is required.'] 375 | 376 | ``error_messages`` lets you customize those messages 377 | 378 | .. doctest:: 379 | 380 | >>> name = forms.CharField( 381 | ... error_messages={'required': 'Please enter your name'}) 382 | >>> name.clean('') 383 | Traceback (most recent call last): 384 | ... 385 | ValidationError: [u'Please enter your name'] 386 | 387 | Error Class 388 | ----------- 389 | 390 | - ``ValidationErrors`` raised are wrapped in a class 391 | - This class controls HTML formatting 392 | - By default, ``ErrorList`` is used: outputs as ``
    `` 393 | - Specify the ``error_class`` kwarg when constructing the form to 394 | override 395 | 396 | Error Class 397 | ----------- 398 | 399 | .. testcode:: 400 | 401 | from django.forms.util import ErrorList 402 | 403 | class ParagraphErrorList(ErrorList): 404 | def __unicode__(self): 405 | return self.as_paragraphs() 406 | 407 | def as_paragraphs(self): 408 | return "

    %s

    " % ( 409 | ",".join(e for e in self.errors) 410 | ) 411 | 412 | form = ContactForm(data=form_data, error_class=ParagraphErrorList) 413 | 414 | Multiple Forms 415 | -------------- 416 | 417 | Avoid potential name collisions with ``prefix``: 418 | 419 | .. testcode:: 420 | 421 | contact_form = ContactForm(prefix='contact') 422 | 423 | Adds the prefix to HTML name and ID:: 424 | 425 | 426 | 428 | 429 | 431 | 433 | 435 | 436 | Forms for Models 437 | ================ 438 | 439 | Model Forms 440 | ----------- 441 | 442 | - ModelForms map a Model to a Form 443 | - Validation includes Model validators by default 444 | - Supports creating and editing instances 445 | - Key differences from Forms: 446 | 447 | - A field for the Primary Key (usually ``id``) 448 | - ``.save()`` method 449 | - ``.instance`` property 450 | 451 | Model Forms 452 | ----------- 453 | 454 | :: 455 | 456 | from django.db import models 457 | from django import forms 458 | 459 | class Contact(models.Model): 460 | name = models.CharField(max_length=100) 461 | email = models.EmailField() 462 | notes = models.TextField() 463 | 464 | class ContactForm(forms.ModelForm): 465 | class Meta: 466 | model = Contact 467 | 468 | Limiting Fields 469 | --------------- 470 | 471 | - You don't need to expose all the fields in your form 472 | - You can either specify fields to expose, or fields to exclude 473 | 474 | :: 475 | 476 | class ContactForm(forms.ModelForm): 477 | 478 | class Meta: 479 | model = Contact 480 | fields = ('name', 'email',) 481 | 482 | 483 | 484 | class ContactForm(forms.ModelForm): 485 | 486 | class Meta: 487 | model = Contact 488 | exclude = ('notes',) 489 | 490 | Overriding Fields 491 | ----------------- 492 | 493 | - Django will generate fields and widgets based on the model 494 | - These can be overridden, as well 495 | 496 | :: 497 | 498 | class ContactForm(forms.ModelForm): 499 | 500 | name = forms.CharField(widget=forms.TextInput) 501 | 502 | class Meta: 503 | model = Contact 504 | 505 | 506 | Instantiating Model Forms 507 | ------------------------- 508 | 509 | :: 510 | 511 | model_form = ContactForm() 512 | 513 | model_form = ContactForm( 514 | instance=Contact.objects.get(id=2) 515 | ) 516 | 517 | ModelForm.is_valid() 518 | -------------------- 519 | 520 | .. blockdiag:: 521 | 522 | blockdiag { 523 | // Set labels to nodes. 524 | A [label = "Field Validation"]; 525 | C [label = "Form Validation"]; 526 | D [label = "_post_clean()"]; 527 | 528 | A -> C -> D; 529 | } 530 | 531 | - Model Forms have an additional method, ``_post_clean()`` 532 | - Sets cleaned fields on the Model instance 533 | - Called *regardless* of whether the form is valid 534 | 535 | Testing 536 | ------- 537 | 538 | :: 539 | 540 | class ModelFormTests(unittest.TestCase): 541 | def test_validation(self): 542 | form_data = { 543 | 'name': 'Test Name', 544 | } 545 | 546 | form = ContactForm(data=form_data) 547 | self.assert_(form.is_valid()) 548 | self.assertEqual(form.instance.name, 'Test Name') 549 | 550 | form.save() 551 | 552 | self.assertEqual( 553 | Contact.objects.get(id=form.instance.id).name, 554 | 'Test Name' 555 | ) 556 | 557 | 558 | Form Sets 559 | ========= 560 | 561 | Form Sets 562 | --------- 563 | 564 | - Handles multiple copies of the same form 565 | - Adds a unique prefix to each form:: 566 | 567 | form-1-name 568 | 569 | - Support for insertion, deletion, and ordering 570 | 571 | 572 | Defining Form Sets 573 | ------------------ 574 | 575 | .. testcode:: 576 | 577 | from django.forms import formsets 578 | 579 | ContactFormSet = formsets.formset_factory( 580 | ContactForm, 581 | ) 582 | 583 | .. testcode:: 584 | :hide: 585 | 586 | request = request_factory.post( 587 | '/', 588 | rebar.testing.flatten_to_dict(ContactFormSet()), 589 | ) 590 | 591 | .. testcode:: 592 | 593 | formset = ContactFormSet(data=request.POST) 594 | 595 | Factory kwargs: 596 | 597 | - ``can_delete`` 598 | - ``extra`` 599 | - ``max_num`` 600 | 601 | Using Form Sets 602 | --------------- 603 | 604 | :: 605 | 606 |
    607 | {% formset %} 608 |
    609 | 610 | Or more control over output:: 611 | 612 |
    613 | {% formset.management_form %} 614 | {% for form in formset %} 615 | {% form %} 616 | {% endfor %} 617 |
    618 | 619 | Management Form 620 | --------------- 621 | 622 | - ``formset.management_form`` provides fields for tracking the member 623 | forms 624 | 625 | - ``TOTAL_FORMS`` 626 | - ``INITIAL_FORMS`` 627 | - ``MAX_NUM_FORMS`` 628 | 629 | - Management form data **must** be present to validate a Form Set 630 | 631 | formset.is_valid() 632 | ------------------ 633 | 634 | .. blockdiag:: 635 | 636 | blockdiag { 637 | // Set labels to nodes. 638 | A [label = "Clean Fields"]; 639 | B [label = "Clean Form"]; 640 | C [label = "Clean FormSet"]; 641 | 642 | A -> B -> C; 643 | B -> A; 644 | } 645 | 646 | - Performs validation on each member form 647 | - Calls ``.clean()`` method on the FormSet 648 | - ``formset.clean()`` can be overridden to validate across Forms 649 | - Errors raised are collected in ``formset.non_form_errors()`` 650 | 651 | FormSet.clean() 652 | --------------- 653 | 654 | .. testcode:: 655 | 656 | from django.forms import formsets 657 | 658 | class BaseContactFormSet(formsets.BaseFormSet): 659 | def clean(self): 660 | names = [] 661 | for form in self.forms: 662 | if form.cleaned_data.get('name') in names: 663 | raise ValidationError() 664 | names.append(form.cleaned_data.get('name')) 665 | 666 | ContactFormSet = formsets.formset_factory( 667 | ContactForm, 668 | formset=BaseContactFormSet 669 | ) 670 | 671 | Insertion 672 | --------- 673 | 674 | - FormSets use the ``management_form`` to determine how many forms to 675 | build 676 | - You can add more by creating a new form and incrementing 677 | ``TOTAL_FORM_COUNT`` 678 | - ``formset.empty_form`` provides an empty copy of the form with 679 | ``__prefix__`` as the index 680 | 681 | .. Insertion HTML 682 | .. -------------- 683 | 684 | .. XXX 685 | 686 | Deletion 687 | -------- 688 | 689 | - When deletion is enabled, additional ``DELETE`` field is added to 690 | each form 691 | - Forms flagged for deletion are available using the 692 | ``.deleted_forms`` property 693 | - Deleted forms are **not** validated 694 | 695 | :: 696 | 697 | ContactFormSet = formsets.formset_factory( 698 | ContactForm, can_delete=True, 699 | ) 700 | 701 | 702 | Ordering Forms 703 | -------------- 704 | 705 | - When ordering is enabled, additional ``ORDER`` field is added to 706 | each form 707 | - Forms are available (in order) using the ``.ordered_forms`` property 708 | 709 | :: 710 | 711 | ContactFormSet = formsets.formset_factory( 712 | ContactForm, 713 | can_order=True, 714 | ) 715 | 716 | Testing 717 | ------- 718 | 719 | - FormSets can be tested in the same way as Forms 720 | - Helpers to generate test form data: 721 | 722 | - ``flatten_to_dict`` works with FormSets just like Forms 723 | - ``empty_form_data`` takes a FormSet and index, returns a dict of data 724 | for an empty form: 725 | 726 | .. testcode:: 727 | 728 | from rebar.testing import flatten_to_dict, empty_form_data 729 | 730 | formset = ContactFormSet() 731 | form_data = flatten_to_dict(formset) 732 | form_data.update( 733 | empty_form_data(formset, len(formset)) 734 | ) 735 | 736 | 737 | Model Form Sets 738 | --------------- 739 | 740 | - ModelFormSets:FormSets :: ModelForms:Forms 741 | - ``queryset`` argument specifies initial set of objects 742 | - ``.save()`` returns the list of saved instances 743 | - If ``can_delete`` is ``True``, ``.save()`` also deletes the models 744 | flagged for deletion 745 | 746 | Advanced & Miscellaneous Detritus 747 | ================================= 748 | 749 | Localizing Fields 750 | ----------------- 751 | 752 | - Django's i18n/l10n framework supports localized input formats 753 | - For example: 10,00 vs. 10.00 754 | 755 | Enable in ``settings.py``:: 756 | 757 | USE_L10N = True 758 | USE_THOUSAND_SEPARATOR = True # optional 759 | 760 | Localizing Fields Example 761 | ------------------------- 762 | 763 | And then use the ``localize`` kwarg 764 | 765 | .. testsetup:: l10n 766 | 767 | from django.conf import settings 768 | settings.USE_L10N = True 769 | 770 | .. doctest:: l10n 771 | 772 | >>> from django import forms 773 | >>> class DateForm(forms.Form): 774 | ... pycon_ends = forms.DateField(localize=True) 775 | 776 | >>> DateForm({'pycon_ends': '3/15/2012'}).is_valid() 777 | True 778 | >>> DateForm({'pycon_ends': '15/3/2012'}).is_valid() 779 | False 780 | 781 | >>> from django.utils import translation 782 | >>> translation.activate('en_GB') 783 | >>> DateForm({'pycon_ends':'15/3/2012'}).is_valid() 784 | True 785 | 786 | Dynamic Forms 787 | ------------- 788 | 789 | - Declarative syntax is just sugar 790 | - Forms use a metaclass to populate ``form.fields`` 791 | - After ``__init__`` finishes, you can manipulate ``form.fields`` 792 | without impacting other instances 793 | 794 | 795 | State Validators 796 | ---------------- 797 | 798 | - Validation isn't necessarily all or nothing 799 | - State Validators define validation for specific states, on top of 800 | basic validation 801 | - Your application can take action based on whether the form is valid, 802 | or valid for a particular state 803 | 804 | 805 | State Validators 806 | ---------------- 807 | 808 | .. testcode:: 809 | 810 | from django import forms 811 | from rebar.validators import StateValidator, StateValidatorFormMixin 812 | 813 | class PublishValidator(StateValidator): 814 | validators = { 815 | 'title': lambda x: bool(x), 816 | } 817 | 818 | class EventForm(StateValidatorFormMixin, forms.Form): 819 | state_validators = { 820 | 'publish': PublishValidator, 821 | } 822 | title = forms.CharField(required=False) 823 | 824 | State Validators 825 | ---------------- 826 | 827 | :: 828 | 829 | >>> form = EventForm(data={}) 830 | >>> form.is_valid() 831 | True 832 | >>> form.is_valid('publish') 833 | False 834 | >>> form.errors('publish') 835 | {'title': 'This field is required'} 836 | -------------------------------------------------------------------------------- /further-reading.rst: -------------------------------------------------------------------------------- 1 | .. slideconf:: 2 | :theme: single-level 3 | 4 | Further Reading 5 | =============== 6 | 7 | * `Django Doesn't Scale`_, by Jacob Kaplan Moss 8 | 9 | .. _`Django Doesn't Scale`: http://www.oscon.com/oscon2012/public/schedule/detail/24030 10 | -------------------------------------------------------------------------------- /handouts/Effective-Django-OSCON-2013.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/handouts/Effective-Django-OSCON-2013.pdf -------------------------------------------------------------------------------- /handouts/Effective-Django-PyCon-2013.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nyergler/effective-django/739217484b6fbbc115af683ad5928e343ee5fb20/handouts/Effective-Django-PyCon-2013.pdf -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. slideconf:: 2 | :autoslides: True 3 | :theme: single-level 4 | 5 | ================== 6 | Effective Django 7 | ================== 8 | 9 | .. note:: 10 | 11 | There's new content and tutorials coming! 12 | 13 | `Sign up `_ to be notified with *Effective 14 | Django* is updated. 15 | 16 | .. note:: 17 | 18 | `Video of the tutorial from PyCon`_ is available on YouTube. 19 | 20 | .. _`Video of the tutorial from PyCon`: https://www.youtube.com/watch?v=NfsJDPm0X54 21 | 22 | Django is a popular, powerful web framework for Python. It has lots of 23 | "batteries" included, and makes it easy to get up and going. But all 24 | of the power means you can write low quality code that still seems to 25 | work. *Effective Django* development means building applications that 26 | are testable, maintainable, and scalable -- not only in terms of 27 | traffic or load, but in terms of being able to add developers to 28 | projects. After reading *Effective Django* you should have an 29 | understanding of how Django's pieces fit together, how to use them to 30 | engineer web applications, and where to look to dig deeper. 31 | 32 | These documents are a combination of the notes and examples developed 33 | for talks prepared for PyCon 2012, PyOhio 2012, and PyCon 2013, and 34 | for Eventbrite web engineering. I'm still working on fleshing them out 35 | into a single document, but I hope you find them useful. 36 | 37 | Feedback, suggestions, and questions may be sent to 38 | nathan@yergler.net. You can find (and fork) the source on `github 39 | `_. 40 | 41 | These documents are available in PDF_ and EPub_ format, as well. 42 | 43 | .. _PDF: /latex/EffectiveDjango.pdf 44 | .. _EPub: /epub/EffectiveDjango.epub 45 | 46 | .. slides:: 47 | 48 | .. figure:: /_static/building.jpg 49 | :class: fill 50 | 51 | CC BY-NC-SA http://www.flickr.com/photos/t_lawrie/278932896/ 52 | 53 | http://effectivedjango.com 54 | 55 | Nathan Yergler // nathan@yergler.net // @nyergler 56 | 57 | 58 | What do you mean, "Effective"? 59 | ============================== 60 | 61 | * Testable 62 | * Maintainable 63 | * Scalable 64 | 65 | Contents 66 | ======== 67 | 68 | .. toctree:: 69 | :maxdepth: 2 70 | 71 | intro.rst 72 | tutorial/index.rst 73 | testing.rst 74 | middleware.rst 75 | Databases & Models 76 | classbasedviews.rst 77 | Forms 78 | acknowledgments.rst 79 | further-reading.rst 80 | 81 | Everything In Its Right Place 82 | ============================= 83 | 84 | .. Table:: 85 | :class: context-table 86 | 87 | +-------------------------+---------------------------------+ 88 | | **Views** | Convert Request to Response | 89 | +-------------------------+---------------------------------+ 90 | | **Forms** | Convert input to Python objects | 91 | +-------------------------+---------------------------------+ 92 | | **Models** | Data and business logic | 93 | +-------------------------+---------------------------------+ 94 | -------------------------------------------------------------------------------- /intro.rst: -------------------------------------------------------------------------------- 1 | .. slideconf:: 2 | :autoslides: False 3 | :theme: single-level 4 | 5 | ============ 6 | Introduction 7 | ============ 8 | 9 | In the past decade the Python community has seen a wealth of riches 10 | spring up in the area of web development. Frameworks and tools have 11 | made it easier than ever to use Python for web applications, with some 12 | focused on particular domains, others on particular footprints, and 13 | still others on particular deployment strategies. Most of these 14 | frameworks have built upon WSGI_, the Web Server Gateway Interface, 15 | which became part of the Python standard library in version TK. WSGI 16 | provides some conventions for applications and servers to communicate 17 | with one another, much as it's spiritual predecessor, CGI_, provided 18 | conventions for executing scripts via a web server. 19 | 20 | With the inclusion of WSGI, it's possible to begin developing a web 21 | application by simply picking and choosing pieces that seem best for 22 | the task at hand. Indeed, some projects do just that. So why use a 23 | larger framework like Django_, Pylons_, or `Blue Bream`_? Frameworks 24 | build upon WSGI to provide a reasonable set of defaults, a set of 25 | conventions, for getting started with development and focusing on the 26 | specific problem at hand. It's *possible* to spend time evaluating 27 | libraries that map a URL to a view, but a framework's developers have 28 | already (presumably) done such an evaluation, and chosen one that they 29 | feel will work well with the other parts of the framework. TK:Community 30 | 31 | A framework is general purpose by definition, but that doesn't mean 32 | your use of it must be. Put another way, most frameworks support a 33 | variety of databases, platforms, and deployment infrastructures. But 34 | just because you use that framework doesn't mean you need to, as well. 35 | A good framework will help you get up to speed more quickly, and will 36 | let you target things for your environment when needed. 37 | 38 | Django is a popular, powerful web framework for Python. It has lots of 39 | "batteries" included, and makes it easy to get up and going. But all 40 | of the power means you can write low quality code that still seems to 41 | work. *Effective Django* development means building applications that 42 | are testable, maintainable, and scalable -- not only in terms of 43 | traffic or load, but in terms of being able to add developers to 44 | projects. When we're talking about Effective Django, we're really 45 | talking about software engineering for web applications. The examples 46 | and the details we're going to talk about are Django specific, but the 47 | ideas and principles are not. 48 | 49 | So what does *Effective Django* mean? It means using Django in a way 50 | that emphasizes writing code that's cohesive, testable, and scalable. 51 | What do each of those words mean? Well "cohesive" code is code that is 52 | focused on doing one thing, and one thing alone. It means that when 53 | you write a function or a method, that it does one thing and does it 54 | well. This is directly related to writing testable code: code that's 55 | doing too much is often difficult to write tests for. When I find 56 | myself thinking, "Well, this piece of code is just too complex to 57 | write a test for, it's not really worth all the effort," that's a 58 | signal that I need to step back and focus on simplifying it. Testable 59 | code is code that makes it straight-forward to write tests for, and 60 | that's easy to diagnose problems with. Finally, we want to write 61 | scalable code. That doesn't just mean it scales in terms of 62 | performance, but that it also scales in terms of your team and your 63 | team's understanding. Applications that are well tested are easier for 64 | others to understand (and easier for them to modify), which means 65 | you're more able to improve your application by adding engineers. 66 | 67 | Part of being able to effectively use Django is understanding 68 | what's available to you, and what the restrictions are. Frameworks 69 | are necessarily general purpose tools, which is great: the 70 | abstractions and tools they provide allow us to begin working 71 | immediately, without delving into the details. At some point, 72 | however, it's useful to understand what the framework is doing for 73 | you. Whether you're trying to stretch in a way the framework didn't 74 | imagine, or you're just trying to diagnose a mysterious bug, you 75 | have to look inside the black box and gain a deeper 76 | understanding. After reading *Effective Django* you should have an 77 | understanding of how Django's pieces fit together, how to use them to 78 | engineer web applications, and where to look to dig deeper. 79 | 80 | .. slide:: Format 81 | :level: 2 82 | 83 | * Talk / Demonstrate / Practice 84 | * Please ask questions 85 | * Two breaks planned 86 | * Notes, examples, etc available: http://effectivedjango.com 87 | 88 | My goal is to convince you of the importance of these principles, and 89 | provide examples of how to follow them to build more robust Django 90 | applications. I'm going to walk through building a contact management 91 | application iteratively, building tests as I go. 92 | 93 | .. _WSGI: http://www.python.org/dev/peps/pep-0333/ 94 | .. _CGI: http://en.wikipedia.org/wiki/Common_Gateway_Interface 95 | .. _Django: http://djangoproject.com/ 96 | .. _Pylons: http://www.pylonsproject.org/ 97 | .. _`Blue Bream`: http://bluebream.zope.org/ 98 | -------------------------------------------------------------------------------- /middleware.rst: -------------------------------------------------------------------------------- 1 | .. slideconf:: 2 | :theme: single-level 3 | 4 | ========================== 5 | Understanding Middleware 6 | ========================== 7 | 8 | 9 | Overview of Middleware 10 | ====================== 11 | 12 | * Lightweight "plug-ins" for Django 13 | * Allows modifying the Request or Response, or mutating the View 14 | parameters 15 | * Defined as a sequence (tuple) of classes in ``settings`` 16 | 17 | .. testcode:: 18 | 19 | MIDDLEWARE_CLASSES = ( 20 | 'django.middleware.common.CommonMiddleware', 21 | 'django.contrib.sessions.middleware.SessionMiddleware', 22 | 'django.middleware.csrf.CsrfViewMiddleware', 23 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 24 | 'django.contrib.messages.middleware.MessageMiddleware', 25 | ) 26 | 27 | Middleware Hooks 28 | ================ 29 | 30 | * Middleware classes have hooks for processing: 31 | 32 | - ``request`` 33 | - ``response`` 34 | - ``view`` 35 | - ``template_response`` 36 | - ``exception`` 37 | 38 | * Individual middleware may implement some or all 39 | 40 | Typical Uses 41 | ============ 42 | 43 | * Sessions 44 | * Authentication 45 | * CSRF Protection 46 | * GZipping Content 47 | 48 | Middleware Example 49 | ================== 50 | 51 | .. testcode:: 52 | 53 | class LocaleMiddleware(object): 54 | 55 | def process_request(self, request): 56 | 57 | if 'locale' in request.cookies: 58 | request.locale = request.cookies.locale 59 | else: 60 | request.locale = None 61 | 62 | def process_response(self, request, response): 63 | 64 | if getattr(request, 'locale', False): 65 | response.cookies['locale'] = request.locale 66 | 67 | 68 | Request Middleware 69 | ================== 70 | 71 | * On ingress, middleware is executed in order 72 | * Request middleware returns ``None`` to continue processing 73 | * Returning an ``HttpResponse`` short circuits additional middleware 74 | 75 | Response Middleware 76 | =================== 77 | 78 | * On egress, middleware is executed in reverse order 79 | * Response middleware is executed *even if corresponding request 80 | middleware not executed* 81 | 82 | Writing Your Own 83 | ================ 84 | 85 | * Simple Python Classes 86 | * Can implement all or part of the interface 87 | * Middleware is long-lived 88 | * The place for storing request-specific information is cunningly 89 | named ``request`` 90 | 91 | WSGI Middleware 92 | =============== 93 | 94 | * WSGI_ also defines a middleware interface 95 | * The two have similar functions, but are **not** the same 96 | 97 | .. _WSGI: http://wsgi.org 98 | -------------------------------------------------------------------------------- /orm.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Effective Django ORM 3 | ====================== 4 | 5 | Configuring the Database 6 | ======================== 7 | 8 | Writing Models 9 | ============== 10 | 11 | .. testcode:: 12 | 13 | from django.db import models 14 | 15 | class Address(models.Model): 16 | 17 | address = models.CharField(max_length=255, blank=True) 18 | city = models.CharField(max_length=150, blank=True) 19 | state = models.CharField(max_length=2, blank=True) 20 | zip = models.CharField(max_length=15, blank=True) 21 | 22 | class Contact(models.Model): 23 | 24 | first_name = models.CharField(max_length=255, blank=True) 25 | last_name = models.CharField(max_length=255, blank=True) 26 | 27 | birthdate = models.DateField(auto_now_add=True) 28 | phone = models.CharField(max_length=25, blank=True) 29 | email = models.EMailField(blank=True) 30 | 31 | address = models.ForeignKey(Address, null=True) 32 | 33 | Working with Models 34 | =================== 35 | 36 | .. testcode:: 37 | 38 | nathan = Contact() 39 | nathan.first_name = 'Nathan' 40 | nathan.last_name = 'Yergler' 41 | nathan.save() 42 | 43 | What Goes in Models 44 | =================== 45 | 46 | * Models should encapsulate business logic 47 | * Encourages testable, composable code 48 | * If logic operates on a "set" of Models, put it in the Manager 49 | 50 | Saving Data 51 | =========== 52 | 53 | * ``.save()`` update the entire model, even if just some fields have changed. 54 | * `django-dirtyfields`_ helps you track which fields have changed, and save 55 | only these fields with its custom ``.save_dirty_fields()`` method. 56 | 57 | Managers 58 | ======== 59 | 60 | * Models get a manager injected as ``.objects`` 61 | * Managers allow you to operate over collections of your model 62 | * Default manager emulates part of the ``QuerySet`` API for 63 | convenience 64 | 65 | .. testcode:: 66 | 67 | Contact.objects.filter(last_name__iexact='yergler') 68 | Contact.objects.filter(address__state='OH') 69 | 70 | Custom Managers 71 | --------------- 72 | 73 | * You can override the default Manager, or add additional ones 74 | * Operations on sets of Model instances belongs here 75 | * Subclass from ``models.Manager`` to get queryset emulation 76 | 77 | .. testcode:: 78 | 79 | class ContactManager(models.Manager): 80 | 81 | def with_email(self): 82 | return self.filter(email__ne = '') 83 | 84 | class Contact(models.Model): 85 | ... 86 | 87 | objects = ContactManager() 88 | 89 | .. testcode:: 90 | 91 | contacts.objects.with_email().filter(email__endswith='osu.edu') 92 | 93 | Low-level Managers 94 | ------------------ 95 | 96 | * Sometimes you want to heavily customize the manager without 97 | re-implementing everything 98 | * ``Manager.get_query_set()`` 99 | allows you to customize the basic QuerySet used by Manager methods 100 | 101 | Testing 102 | ======= 103 | 104 | What to Test 105 | ------------ 106 | 107 | * Business logic methods 108 | * Customized Manager methods 109 | 110 | Writing a Test 111 | -------------- 112 | 113 | .. testcode:: 114 | 115 | def test_with_email(): 116 | 117 | # make a couple Contacts 118 | Contact.objects.create(first_name='Nathan') 119 | Contact.objects.create(email='nathan@eventbrite.com') 120 | 121 | self.assertEqual( 122 | len(Contact.objects.with_email()), 1 123 | ) 124 | 125 | 126 | Test Objects 127 | ------------ 128 | 129 | * Creating objects for tests is time consuming 130 | * Unnecessarily involves the database 131 | * `factory boy`_ provides an easy way to make model factories 132 | 133 | FactoryBoy Example 134 | ------------------ 135 | 136 | .. testcode:: 137 | 138 | import factory 139 | from models import Contact 140 | 141 | class ContactFactory(factory.Factory): 142 | FACTORY_FOR = Contact 143 | 144 | first_name = 'John' 145 | last_name = 'Doe' 146 | 147 | # Returns a Contact instance that's not saved 148 | contact = ContactFactory.build() 149 | contact = ContactFactory.build(last_name='Yergler') 150 | 151 | # Returns a saved Contact instance 152 | contact = ContactFactory.create() 153 | 154 | SubFactories for Related Objects 155 | -------------------------------- 156 | 157 | .. testcode:: 158 | 159 | class AddressFactory(factory.Factory): 160 | FACTORY_FOR = Address 161 | 162 | contact = factory.SubFactory(ContactFactory) 163 | 164 | .. testcode:: 165 | 166 | address = AddressFactory(city='Columbus', state='OH') 167 | address.contact.first_name 168 | 169 | .. testoutput:: 170 | 171 | 'John' 172 | 173 | Querying Your Data 174 | ================== 175 | 176 | * Query Sets are chainable 177 | 178 | .. testcode:: 179 | 180 | Contact.objects.filter(state='OH').filter(email__ne='') 181 | 182 | * Multiple filters are collapsed into SQL "and" conditions 183 | 184 | OR conditions in Queries 185 | ------------------------ 186 | 187 | If you need to do "or" conditions, you can use ``Q`` objects 188 | 189 | .. testcode:: 190 | 191 | from django.db.models import Q 192 | 193 | Contact.objects.filter( 194 | Q(state='OH') | Q(email__endswith='osu.edu') 195 | ) 196 | 197 | .. F objects let you refer to fields in the same object 198 | .. ---------------------------------------------------- 199 | 200 | .. XXX 201 | 202 | 203 | ORM Performance 204 | =============== 205 | 206 | 207 | Instantiation is Expensive 208 | -------------------------- 209 | 210 | :: 211 | 212 | for user in Users.objects.filter(is_active=True): 213 | send_email(user.email) 214 | 215 | * QuerySets are lazy, but have non-trivial overhead when evaluated 216 | * If a query returns 1000s of rows, users will notice this 217 | * ``.values()`` and ``.values_list()`` avoid instantiation 218 | 219 | Avoiding Instantiation 220 | ---------------------- 221 | 222 | :: 223 | 224 | user_emails = Users.objects.\ 225 | filter(is_active=True).\ 226 | values_list('email', flat=True) 227 | 228 | for email in user_emails: 229 | send_email(email) 230 | 231 | 232 | Traversing Relationships 233 | ------------------------ 234 | 235 | * Traversing foreign keys can incur additional queries 236 | * ``select_related`` queries for foreign keys in the initial query 237 | 238 | .. testcode:: 239 | 240 | Contact.objects.\ 241 | select_related('address').\ 242 | filter(last_name = 'Yergler') 243 | 244 | 245 | Query Performance 246 | ----------------- 247 | 248 | * QuerySets maintain state in memory 249 | * Chaining triggers cloning, duplicating that state 250 | * Unfortunately, QuerySets maintain a *lot* of state 251 | * If possible, don't chain more than one filter 252 | 253 | Falling Back to Raw SQL 254 | ----------------------- 255 | 256 | * Django has to be database agnostic, you don't 257 | * Sometimes the clearest thing to do is write a SQL statement 258 | * The ``.raw()`` method lets you do this 259 | 260 | .. testcode:: 261 | 262 | Contact.objects.raw('SELECT * FROM contacts WHERE last_name = %s', [lname]) 263 | 264 | * Must retrieve the primary key 265 | * Omitted fields will be "deferred" 266 | * **DO NOT** use string formatting in ``raw()`` calls 267 | 268 | Other Manager Operations 269 | ------------------------ 270 | 271 | Managers have some additional helpers for operating on the table or 272 | collection: 273 | 274 | * ``get_or_create`` 275 | * ``update`` 276 | * ``delete`` 277 | * ``bulk_insert`` 278 | 279 | 280 | Read Repeatable 281 | --------------- 282 | 283 | MySQL's default transaction isolation for InnoDB **breaks** 284 | Django's ``get_or_create`` when running at scale 285 | 286 | :: 287 | 288 | def get_or_create(self, **kwargs): 289 | 290 | try: 291 | return self.get(**lookup), False 292 | except self.model.DoesNotExist: 293 | try: 294 | obj = self.model(**params) 295 | obj.save(force_insert=True, using=self.db) 296 | return obj, True 297 | except IntegrityError, e: 298 | try: 299 | return self.get(**lookup), False 300 | except self.model.DoesNotExist: 301 | raise e 302 | 303 | 304 | .. _`django-dirtyfields`: http://pypi.python.org/pypi/django-dirtyfields/ 305 | .. _`factory boy`: http://pypi.python.org/pypi/factory_boy 306 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.5.4 2 | Sphinx~=1.3 3 | rebar==0.1 4 | sphinxcontrib-blockdiag~=1.5 5 | hieroglyph 6 | tut 7 | -------------------------------------------------------------------------------- /scratchpad/form-resources.rst: -------------------------------------------------------------------------------- 1 | 2 | * django-form-utils 3 | http://pypi.python.org/pypi/django-form-utils/0.2.0 4 | * crispy forms 5 | * rebar 6 | -------------------------------------------------------------------------------- /scratchpad/gettingstarted.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting Started 3 | =============== 4 | 5 | Your Application Environment 6 | ============================ 7 | 8 | Starting a Project 9 | ------------------ 10 | 11 | Starting a project is easy:: 12 | 13 | $ python django-admin.py startproject 14 | 15 | What comes next? Or right before? 16 | 17 | 18 | Deployment From Day 1 19 | --------------------- 20 | 21 | * Pretend you need to deploy from Day 1 22 | * And assume that you want that automated 23 | 24 | Dependency Management 25 | --------------------- 26 | 27 | * Choose a tool to track dependencies, use it in development 28 | 29 | * pip w/requirements.txt 30 | * virtualenv 31 | * buildout 32 | 33 | * Identify specific versions of dependencies (by number or SHA) 34 | 35 | Your Environment 36 | ---------------- 37 | 38 | * If you're on your own, just use a virtualenv 39 | * If you're working with an ops person/team, consider Vagrant from the 40 | start 41 | * Even if you don't use Puppet to configure the dev VM, at least 42 | you're running code on another "machine". 43 | 44 | 45 | Beginning a Django Project 46 | ========================== 47 | 48 | Scaffolding 49 | ----------- 50 | 51 | * Django provides file system scaffolding just like HTTP scaffolding 52 | * Helps engineers understand where to find things when they go looking 53 | * Django 1.4 made a change that decouples apps from projects 54 | * In Django parlance, your **project** is a collections of **applications**. 55 | 56 | Creating the Scaffolding 57 | ------------------------ 58 | 59 | :: 60 | 61 | $ python django-admin.py startproject contactmgr 62 | 63 | :: 64 | 65 | ./contactmgr 66 | manage.py 67 | ./contactmgr 68 | __init__.py 69 | settings.py 70 | urls.py 71 | wsgi.py 72 | 73 | .. notslides:: 74 | 75 | * ``manage.py`` is a pointer back to ``django-admin.py`` with an 76 | environment variable set, pointing to your project as the one to 77 | read settings from and operate on when needed. 78 | * ``settings.py`` is where you'll configure your app. It has a few 79 | sensible defaults, but no database chosen when you start. 80 | * ``urls.py`` contains the URL to view mappings: we'll talk more about 81 | that shortly. 82 | * ``wsgi.py`` is WSGI_ wrapper for your application. This is used by 83 | Django's development servers, and possibly (hopefully) other 84 | containers like mod_wsgi, uwsgi, etc. 85 | 86 | ...and the "App" 87 | ---------------- 88 | 89 | :: 90 | 91 | $ python ./contactmgr/manage.py startapp contacts 92 | 93 | :: 94 | 95 | ./contactmgr 96 | ./contacts 97 | __init__.py 98 | models.py 99 | tests.py 100 | views.py 101 | 102 | .. notslides:: 103 | 104 | * Beginning in Django 1.4, *apps* are placed alongside *project* 105 | packages. This is a great improvement when it comes to 106 | deployment. 107 | * ``models.py`` will contain the Django ORM models for your app. 108 | * ``views.py`` will contain the View code 109 | * ``tests.py`` will contain the unit and integration tests you 110 | write. 111 | 112 | .. _WSGI: http://www.python.org/dev/peps/pep-0333/ 113 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /testing.rst: -------------------------------------------------------------------------------- 1 | .. slideconf:: 2 | :theme: single-level 3 | 4 | =================== 5 | Testing in Django 6 | =================== 7 | 8 | Testing Django 9 | ============== 10 | 11 | * There are Unit Tests and there are Integration tests 12 | * Unit Tests should not rely on external services 13 | * Unit Tests should be *fast* 14 | 15 | Writing a Unit Test 16 | =================== 17 | 18 | * Django bundles ``unittest2`` as ``django.utils.unittest`` 19 | 20 | :: 21 | 22 | import django.http 23 | import django.utils.unittest as unittest2 24 | 25 | class LocaleMiddlewareTests(unittest2.TestCase): 26 | 27 | def test_request_not_processed(self): 28 | 29 | middleware = LocaleMiddle() 30 | response = django.http.HttpResponse() 31 | middleware.process_response(none, response) 32 | 33 | self.assertFalse(response.cookies) 34 | 35 | Test Client 36 | =========== 37 | 38 | * Django TestClient acts like a browser. Sort of. 39 | * Allows you to make a request against your application and inspect 40 | the response 41 | * The TestClient is *slow* (compared to plain unit tests) 42 | 43 | .. testcode:: 44 | 45 | from django.test.client import Client 46 | 47 | c = Client() 48 | 49 | response = c.get('/login') 50 | self.assertEqual(response.status_code, 200) 51 | 52 | response = c.post('/login/', {'username': 'john', 'password': 'smith'}) 53 | 54 | Request Factory 55 | =============== 56 | 57 | * Django 1.3 introduced ``RequestFactory``, with an API similar to 58 | Test Client 59 | * Easy way to generate ``Request`` objects, which can be passed to 60 | views 61 | * Note that middleware is **not** run on these Requests 62 | 63 | Running Tests 64 | ============= 65 | 66 | * Django only looks in apps with ``models.py`` for tests 67 | 68 | :: 69 | 70 | $ ./manage.py test 71 | 72 | * Easy to replace the test runner with something like ``nose`` if you 73 | so desire 74 | 75 | 76 | Further Reading 77 | =============== 78 | 79 | * `Django Testing Documentation`_ 80 | * `Django 1.1 Testing & Debugging`_ 81 | 82 | 83 | .. _`Django Testing Documentation`: https://docs.djangoproject.com/en/1.4/topics/testing/ 84 | .. _`Django 1.1 Testing & Debugging`: http://www.packtpub.com/django-1-1-testing-and-debugging/book 85 | -------------------------------------------------------------------------------- /tutorial/additional-views.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | 5 | .. slideconf:: 6 | :autoslides: True 7 | :theme: single-level 8 | 9 | ======================== 10 | Additional Generic Views 11 | ======================== 12 | 13 | Edit Views 14 | ========== 15 | 16 | .. checkpoint:: edit_contact_view 17 | 18 | In addition to creating Contacts, we'll of course want to edit them. 19 | As with the List and Create views, Django has a generic view we can 20 | use as a starting point. 21 | 22 | .. literalinclude:: /src/contacts/views.py 23 | :prepend: from django.views.generic import UpdateView 24 | ... 25 | :pyobject: UpdateContactView 26 | :end-before: def get_context_data 27 | 28 | * we can re-use the same template 29 | * but how does it know which contact to load? 30 | * we need to either: provide a pk/slug, or override get_object(). 31 | * we'll provide pk in the URL 32 | 33 | .. literalinclude:: /src/addressbook/urls.py 34 | :lines: 12-13 35 | 36 | We'll update the contact list to include an edit link next to each 37 | contact. 38 | 39 | .. literalinclude:: /src/contacts/templates/contact_list.html 40 | :language: html 41 | 42 | Note the use of ``pk=contact.id`` in the ``{% url %}`` tag to specify 43 | the arguments to fill into the URL pattern. 44 | 45 | If you run the server now, you'll see an edit link. Go ahead and click 46 | it, and try to make a change. You'll notice that instead of editing 47 | the existing record, it creates a new one. Sad face. 48 | 49 | If we look at the source of the edit HTML, we can easily see the 50 | reason: the form targets ``/new``, not our edit URL. To fix this -- 51 | and still allow re-using the template -- we're going to add some 52 | information to the template context. 53 | 54 | The template context is the information available to a template when 55 | it's rendered. This is a combination of information you provide in 56 | your view -- either directly or indirectly -- and information added by 57 | `context processors`_, such as the location for static media and 58 | current locale. In order to use the same template for add and edit, 59 | we'll add information about where the form should redirect to the 60 | context. 61 | 62 | .. literalinclude:: /src/contacts/views.py 63 | :pyobject: CreateContactView 64 | 65 | .. literalinclude:: /src/contacts/views.py 66 | :pyobject: UpdateContactView 67 | 68 | We also update the template to use that value for the action and 69 | change the title based on whether or not we've previously saved. 70 | 71 | .. literalinclude:: /src/contacts/templates/edit_contact.html 72 | :lines: 5-11 73 | :language: html 74 | 75 | You may wonder where the ``contact`` value in the contact comes from: 76 | the class based views that wrap a single object (those that take 77 | a primary key or slug) expose that to the context in two different 78 | ways: as a variable named ``object``, and as a variable named after 79 | the model class. The latter often makes your templates easier to read 80 | and understand later. You can customize this name by overriding 81 | ``get_context_object_name`` on your view. 82 | 83 | .. sidebar:: Made a Change? Run the Tests. 84 | 85 | We've just made a change to our ``CreateContactView``, which means 86 | this is a perfect time to run the tests we wrote. Do they still pass? 87 | If not, did we introduce a bug, or did the behavior change in a way 88 | that we expected? 89 | 90 | (Hint: We changed how the contact list is rendered, so our tests 91 | that just expect the name there are going to fail. This is a case 92 | where you'd need to update the test case, but it also demonstrates 93 | how integration tests can be fragile.) 94 | 95 | Deleting Contacts 96 | ================= 97 | 98 | .. checkpoint:: delete_contact_view 99 | 100 | The final view for our basic set of views is delete. The generic 101 | deletion view is very similar to the edit view: it wraps a single 102 | object and requires that you provide a URL to redirect to on success. 103 | When it processes a HTTP GET request, it displays a confirmation page, 104 | and when it receives an HTTP DELETE or POST, it deletes the object and 105 | redirects to the success URL. 106 | 107 | We add the view definition to ``views.py``: 108 | 109 | .. literalinclude:: /src/contacts/views.py 110 | :prepend: from django.views.generic import DeleteView 111 | ... 112 | :pyobject: DeleteContactView 113 | 114 | And create the template, ``delete_contact.html``, in our ``templates`` 115 | directory. 116 | 117 | .. literalinclude:: /src/contacts/templates/delete_contact.html 118 | :language: html 119 | 120 | Of course we need to add this to the URL definitions: 121 | 122 | .. literalinclude:: /src/addressbook/urls.py 123 | :lines: 14-15 124 | 125 | And we'll add the link to delete to the edit page. 126 | 127 | .. literalinclude:: /src/contacts/templates/edit_contact.html 128 | :lines: 19-21 129 | 130 | Detail View 131 | =========== 132 | 133 | .. checkpoint:: contact_detail_view 134 | 135 | Finally, let's go ahead and add a detail view for our Contacts. This 136 | will show the details of the Contact: not much right now, but we'll 137 | build on this shortly. Django includes a generic ``DetailView``: think 138 | of it as the single serving ``ListView``. 139 | 140 | .. literalinclude:: /src/contacts/views.py 141 | :prepend: from django.views.generic import DetailView 142 | ... 143 | :pyobject: ContactView 144 | 145 | Again, the template is pretty straight forward; we create 146 | ``contact.html`` in the ``templates`` directory. 147 | 148 | .. literalinclude:: /src/contacts/templates/contact.html 149 | :language: html 150 | 151 | And add the URL mapping: 152 | 153 | .. literalinclude:: /src/addressbook/urls.py 154 | :lines: 10-11 155 | 156 | We're also going to add a method to our Contact model, 157 | ``get_absolute_url``. ``get_absolute_url`` is a Django convention for 158 | obtaining the URL of a single model instance. In this case it's just 159 | going to be a call to ``reverse``, but by providing this method, our 160 | model will play nicely with other parts of Django. 161 | 162 | .. literalinclude:: /src/contacts/models.py 163 | :prepend: class Contact(models.Model): 164 | ... 165 | :pyobject: Contact.get_absolute_url 166 | 167 | And we'll add the link to the contact from the contact list. 168 | 169 | .. literalinclude:: /src/contacts/templates/contact_list.html 170 | :lines: 7-12 171 | :language: html 172 | 173 | .. ifslides:: 174 | 175 | * Next: :doc:`forms` 176 | 177 | 178 | .. _`Generic Views`: https://docs.djangoproject.com/en/1.5/topics/class-based-views/generic-display/ 179 | .. _`Class Based Views`: https://docs.djangoproject.com/en/1.5/topics/class-based-views/ 180 | .. _View: https://docs.djangoproject.com/en/1.5/ref/class-based-views/base/#view 181 | .. _ListView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-display/#listview 182 | .. _UpdateView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-editing/#updateview 183 | .. _CreateView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-editing/#createview 184 | .. _DeleteView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-editing/#deleteview 185 | .. _DetailView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-display/#detailview 186 | .. _`context processors`: https://docs.djangoproject.com/en/1.5/ref/templates/api/#subclassing-context-requestcontext 187 | .. _`Django Form`: https://docs.djangoproject.com/en/1.5/topics/forms/ 188 | .. _HttpRequest: https://docs.djangoproject.com/en/1.5/ref/request-response/#httprequest-objects 189 | .. _HttpResponse: https://docs.djangoproject.com/en/1.5/ref/request-response/#httpresponse-objects 190 | .. _Client: https://docs.djangoproject.com/en/1.5/topics/testing/overview/#module-django.test.client 191 | .. _RequestFactory: https://docs.djangoproject.com/en/1.5/topics/testing/advanced/#django.test.client.RequestFactory 192 | .. _LiveServerTestCase: https://docs.djangoproject.com/en/1.5/topics/testing/overview/#liveservertestcase 193 | .. _Selenium: http://seleniumhq.org/ 194 | -------------------------------------------------------------------------------- /tutorial/authzn.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | ========================================= 5 | Handling Authentication & Authorization 6 | ========================================= 7 | 8 | .. warning:: 9 | 10 | This page is a work in progress; errors may exist, and additional 11 | content is forthcoming. 12 | 13 | So far we've built a simple contact manager, and added support for a 14 | related model (Addresses). This has shown how to use many of the 15 | basics, but there are a few more things you'd want before exposing 16 | this to the outside world. One of those is authentication and 17 | authorization. Django includes support that works for many projects, 18 | which is what we'll use. 19 | 20 | Authentication 21 | ============== 22 | 23 | .. checkpoint:: authentication 24 | 25 | In order to use the included authentication support, the 26 | ``django.contrib.auth`` and ``django.contrib.sessions`` applications 27 | needs to be included in your project. 28 | 29 | Django enables thes by default when you create a project, as you can 30 | see in ``addressbook/settings.py``. 31 | 32 | .. literalinclude:: /src/addressbook/settings.py 33 | :lines: 118-130 34 | 35 | In addition to installing the application, the middleware needs to be 36 | installed, as well. 37 | 38 | .. literalinclude:: /src/addressbook/settings.py 39 | :lines: 96-104 40 | 41 | 42 | If you'll recall, during the first run of ``syncdb``, Django asked if 43 | we wanted to create a superuser account. It did so because we had the 44 | application installed already. 45 | 46 | The stock Django auth model supports Users_, Groups_, and 47 | Permissions_. This is usually sufficient unless you're integrating 48 | with an existing authentication backend. 49 | 50 | ``django.contrib.auth`` provides a set of views to support the basic 51 | authentication actions such as login, logout, password reset, etc. 52 | Note that it includes *views*, but not *templates*. We'll need to 53 | provide those for our project. 54 | 55 | For this example we'll just add support for login and logout views in 56 | our project. First, add the views to ``addressbook/urls.py``. 57 | 58 | .. literalinclude:: /src/addressbook/urls.py 59 | :lines: 7-9 60 | 61 | Both the login_ and logout_ view have default template names 62 | (``registration/login.html`` and ``registration/logged_out.html``, 63 | respectively). Because these views are specific to our project and not 64 | our re-usable Contacts application, we'll create a new 65 | ``templates/registration`` directory inside of ``addressbook``:: 66 | 67 | $ mkdir -p addressbook/templates/registration 68 | 69 | And tell Django to look in that directory for templates by setting 70 | ``TEMPLATE_DIRS`` in ``addressbook/settings.py``. 71 | 72 | .. literalinclude:: /src/addressbook/settings.py 73 | :lines: 111-116 74 | 75 | Within that directory, first create ``login.html``. 76 | 77 | .. literalinclude:: /src/addressbook/templates/registration/login.html 78 | :language: html 79 | 80 | The login template inherits from our ``base.html`` template, and shows 81 | the login form provided by the view. The hidden ``next`` field allows 82 | the view to redirect the user to the page requested, if the login 83 | request was triggered by a permission failure. 84 | 85 | .. sidebar:: Why no name for the URL patterns? 86 | 87 | XXX 88 | 89 | The logout template, ``logged_out.html``, is simpler. 90 | 91 | .. literalinclude:: /src/addressbook/templates/registration/logged_out.html 92 | :language: html 93 | 94 | All it needs to do is provide a message to let the user know the 95 | logout was successful. 96 | 97 | .. sidebar:: Creating an Admin User 98 | 99 | XXX 100 | 101 | If you run your development server now using ``runserver`` and visit 102 | ``http://localhost:8000/login``, you'll see the login page. If you 103 | login with bogus credentials, you should see an error message. So 104 | let's try logging in with the super user credential you created earlier. 105 | 106 | .. image:: 107 | /_static/tutorial/authz-login-pagenotfound.png 108 | 109 | Wait, what? Why is it visiitng ``/accounts/profile``? We never typed 110 | that. The login view wants to redirect the user to a fixed URL after a 111 | successful login, and the default is ``/accounts/profile``. To 112 | override that, we'll set the ``LOGIN_REDIRECT_URL`` value in 113 | ``addressbook/settings.py`` so that once a user logs in they'll be 114 | redirected to the list of contacts. 115 | 116 | .. literalinclude:: /src/addressbook/settings.py 117 | :lines: 161 118 | 119 | Now that we can log in and log out, it'd be nice to show the logged in 120 | user in the header and links to login/logout in the header. We'll add 121 | that to our ``base.html`` template, since we want that to show up 122 | everywhere. 123 | 124 | .. literalinclude:: /src/contacts/templates/base.html 125 | :language: html 126 | :lines: 8-17 127 | 128 | 129 | Authorization 130 | ============= 131 | 132 | .. checkpoint:: authorization 133 | 134 | Having support for login and logout is nice, but we're not actually 135 | using it right now. So we want to first make our Contact views only 136 | available to authenticated users, and then we'll go on to associated 137 | contacts with specific Users, so the application could be used for 138 | multiple users. 139 | 140 | Django includes a suite a functions and decorators that help you guard 141 | a view based on authentication/authorization. One of the most commonly 142 | used is `login_required`_. Unfortunately, applying view decorators to 143 | class based views remains `a little cumbersome`_. There are 144 | essentially two methods: decorating the URL configuration, and 145 | decorating the class. I'll show how to decorate the class. 146 | 147 | Class based views have a ``dispatch()`` method that's called when an 148 | URL pattern matches. The ``dispatch()`` method looks up the 149 | appropriate method on the class based on the HTTP method and then 150 | calls it. Because we want to protect the views for all HTTP methods, 151 | we'll override and decorate that. 152 | 153 | In ``contacts/views.py`` we'll create a class mixin that ensures the 154 | user is logged in. 155 | 156 | .. literalinclude:: /src/contacts/views.py 157 | :lines: 1-2, 16-21 158 | 159 | This is a *mixin* because it doesn't provide a full implementation of 160 | a view on its own; it needs to be *mixed* with another view to have an 161 | effect. 162 | 163 | Once we have it, we can add it to the class declarations in 164 | ``contacts/views.py``. Each view will have our new ``LoggedInMixin`` 165 | added as the first superclass. For example, ``ListContactView`` will 166 | look as follows. 167 | 168 | .. literalinclude:: /src/contacts/views.py 169 | :pyobject: ListContactView 170 | 171 | Just as ``LOGIN_REDIRECT_URL`` tells Django where to send people 172 | *after* they log in, there's a setting to control where to send them 173 | when they *need* to login. However, this can also be a view name, so 174 | we don't have to bake an explicit URL into the settings. 175 | 176 | .. literalinclude:: /src/addressbook/settings.py 177 | :lines: 162 178 | 179 | Checking Ownership 180 | ------------------ 181 | 182 | Checking that you're logged in is well and good, but to make this 183 | suitable for multiple users we need to add the concept of ownership. 184 | There are three steps for 185 | 186 | #. Record the Owner of each Contact 187 | #. Only show Contacts the logged in user owns in the list 188 | #. Set the Owner when creating a new one 189 | 190 | First, we'll go ahead and add the concept of an Owner to the Contact 191 | model. 192 | 193 | In ``contacts/models.py``, we add an import and another field to our 194 | model. 195 | 196 | .. literalinclude:: /src/contacts/models.py 197 | :prepend: from django.contrib.auth.models import User 198 | ... 199 | :pyobject: Contact 200 | 201 | Because Django doesn't support migrations out of the box, we'll need 202 | to blow away the database and re-run syncdb. 203 | 204 | XXX Perfect segue for talking about South 205 | 206 | Now we need to limit the contact list to only the contacts the logged 207 | in User owns. This gets us into overriding methods that the base view 208 | classes have been handling for us. 209 | 210 | For the list of Contacts, we'll want to override the ``get_queryset`` 211 | method, which returns the `Django QuerySet`_ of objects to be 212 | displayed. 213 | 214 | .. literalinclude:: /src/contacts/views.py 215 | :pyobject: ListContactView 216 | 217 | 218 | The remaining views are responsible for showing only a single object 219 | -- the Contact (or its addresses). For those we'll create another 220 | mixin that enforces authorization. 221 | 222 | .. literalinclude:: /src/contacts/views.py 223 | :prepend: from django.core.exceptions import PermissionDenied 224 | ... 225 | :pyobject: ContactOwnerMixin 226 | 227 | ``ContactOwnerMixin`` overrides the ``get_object()`` method, which is 228 | responsible for getting the object for a view to operate on. If it 229 | can't find one with the specified primary key and owner, it raises the 230 | ``PermissionDenied`` exception. 231 | 232 | .. note:: 233 | 234 | This implementation will return HTTP 403 (Forbidden) whenever it 235 | cannot find the a Contact with the requested ID and owner. This 236 | will mask legitimate 404 (Not Found) errors. 237 | 238 | We'll use the ``ContactOwnerMixin`` in all of our views. For example, 239 | ``ContactView`` will look as follows: 240 | 241 | .. literalinclude:: /src/contacts/views.py 242 | :pyobject: ContactView 243 | 244 | Note that the order of inheritance is important: the superclasses 245 | (``LoggedInMixin``, ``ContactOwnerMixin``, ``DetailView``) will be 246 | checked in the order listed for methods. By placing ``LoggedInMixin`` 247 | first, you're guaranteed that by the time execution reaches 248 | ``ContactOwnerMixin`` and ``DetailView``, you have a logged in, 249 | authenticated user. 250 | 251 | Review 252 | ====== 253 | 254 | * XXX 255 | 256 | .. _`login_required`: https://docs.djangoproject.com/en/1.5/topics/auth/default/#django.contrib.auth.decorators.login_required 257 | .. _`a little cumbersome`: https://docs.djangoproject.com/en/1.5/topics/class-based-views/intro/#decorating-class-based-views 258 | .. _Users: https://docs.djangoproject.com/en/1.6/topics/auth/default/#user-objects 259 | .. _Groups: https://docs.djangoproject.com/en/1.6/topics/auth/default/#groups 260 | .. _Permissions: https://docs.djangoproject.com/en/1.6/topics/auth/default/#permissions-and-authorization 261 | .. _login: https://docs.djangoproject.com/en/1.6/topics/auth/default/#django.contrib.auth.views.login 262 | .. _logout: https://docs.djangoproject.com/en/1.6/topics/auth/default/#django.contrib.auth.views.login 263 | .. _`Django QuerySet`: https://docs.djangoproject.com/en/1.6/ref/class-based-views/mixins-multiple-object/#django.views.generic.list.MultipleObjectMixin.get_queryset 264 | -------------------------------------------------------------------------------- /tutorial/before.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Before You Arrive 3 | =================== 4 | 5 | Thanks for signing up to attend my tutorial, *Effective Django*. To 6 | help things get started smoothly, and avoid possible network issues, 7 | please complete the following beforehand. 8 | 9 | If you have questions, you can email me at nathan@yergler.net. If you 10 | have platform specific issues, I'll try and help you figure them out, 11 | and will update this document with any notes. 12 | 13 | #. Install Python 14 | 15 | I'll be using Python 2.7; you can download it from the official 16 | Python website: http://python.org/download/releases/2.7.5/ 17 | 18 | The latest release of 2.7 is 2.7.5. If you already have 2.7.x 19 | installed, that will work fine. 20 | 21 | #. Install virtualenv and pip 22 | 23 | ``virtualenv`` is a tool for managing your Python environments. It 24 | includes ``pip``, a Python package installer, which we'll use to 25 | retrieve dependencies. 26 | 27 | You can download `virtualenv from PyPI`_ at 28 | https://pypi.python.org/packages/source/v/virtualenv/virtualenv-1.9.1.tar.gz 29 | 30 | After downloading, unpack the archive and run:: 31 | 32 | $ python setup.py install 33 | 34 | You may need to run with sudo depending on your system:: 35 | 36 | $ sudo python setup.py install 37 | 38 | #. Create a working directory for the tutorial, and make it a virtualenv 39 | 40 | You can create this directory anywhere, but this is where we'll be 41 | working on code during the tutorial. For example:: 42 | 43 | $ cd Documents 44 | $ mkdir django-tutorial 45 | 46 | Once you've created the directory, create a virtualenv there:: 47 | 48 | $ virtualenv ./django-tutorial 49 | 50 | #. Active the virtualenv 51 | 52 | On Linux and Mac OS X you can active the virtualenv by running the 53 | following from the command-line:: 54 | 55 | $ cd django-tutorial 56 | $ source bin/activate 57 | 58 | On Windows you do:: 59 | 60 | > cd django-tutorial 61 | > \Scripts\activate 62 | 63 | The `virtualenv documentation`_ may be useful if you have trouble activating. 64 | 65 | #. Install Django 66 | 67 | We'll talk about versions of Django during the tutorial, but in 68 | order to avoid network bottlenecks from dozens of people in one 69 | room downloading the source, it's helpful to install it into your 70 | virtualenv beforehand:: 71 | 72 | $ pip install Django 73 | 74 | This will download Django and install it into the virtualenv. Once 75 | it's installed, you should see a ``django-admin.py`` script in the 76 | ``bin`` (Linux, Mac OS X) or ``Scripts`` (Windows) directory. 77 | 78 | If you have questions, you can email me at nathan@yergler.net. If you 79 | have platform specific issues, I'll try and help you figure them out, 80 | and will update this document with any notes. 81 | 82 | See you at the tutorial! 83 | 84 | Nathan 85 | 86 | 87 | 88 | .. _`virtualenv documentation`: http://www.virtualenv.org/en/latest/#activate-script 89 | .. _`virtualenv from PyPI`: https://pypi.python.org/pypi/virtualenv 90 | -------------------------------------------------------------------------------- /tutorial/forms.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | .. slideconf:: 5 | :autoslides: False 6 | :theme: single-level 7 | 8 | ============= 9 | Form Basics 10 | ============= 11 | 12 | .. slide:: Django Forms 13 | :level: 1 14 | 15 | Validate user input and return Python objects 16 | 17 | .. slide:: Defining Forms 18 | :level: 2 19 | 20 | Forms are composed of fields, which have a widget. 21 | 22 | .. testcode:: 23 | 24 | from django.utils.translation import gettext_lazy as _ 25 | from django import forms 26 | 27 | class ContactForm(forms.Form): 28 | 29 | name = forms.CharField(label=_("Your Name"), 30 | max_length=255, 31 | widget=forms.TextInput, 32 | ) 33 | 34 | email = forms.EmailField(label=_("Email address")) 35 | 36 | .. slide:: Instantiating a Form 37 | :level: 2 38 | 39 | Unbound forms don't have data associated with them, but they can 40 | be rendered:: 41 | 42 | form = ContactForm() 43 | 44 | Bound forms have specific data associated, which can be 45 | validated:: 46 | 47 | form = ContactForm(data=request.POST, files=request.FILES) 48 | 49 | .. slide:: Accessing Fields 50 | :level: 2 51 | 52 | Two ways to access fields on a Form instance 53 | 54 | - ``form.fields['name']`` returns the ``Field`` object 55 | - ``form['name']`` returns a ``BoundField`` 56 | - ``BoundField`` wraps a field and value for HTML output 57 | 58 | .. slide:: Validating the Form 59 | :level: 2 60 | 61 | .. blockdiag:: 62 | 63 | blockdiag { 64 | // Set labels to nodes. 65 | A [label = "Field Validation"]; 66 | C [label = "Form Validation"]; 67 | 68 | A -> C; 69 | } 70 | 71 | - Only bound forms can be validated 72 | - Calling ``form.is_valid()`` triggers validation if needed 73 | - Validated, cleaned data is stored in ``form.cleaned_data`` 74 | - Calling ``form.full_clean()`` performs the full cycle 75 | 76 | .. slide:: Field Validation 77 | :level: 2 78 | 79 | .. blockdiag:: 80 | 81 | blockdiag { 82 | // Set labels to nodes. 83 | A [label = "for each Field"]; 84 | 85 | B [label = "Field.clean"]; 86 | C [label = "Field.to_python"]; 87 | D [label = "Field validators"]; 88 | 89 | F [label = ".clean_fieldname()"]; 90 | 91 | A -> B; 92 | B -> C; 93 | C -> D; 94 | 95 | A -> F; 96 | } 97 | 98 | - Three phases for Fields: To Python, Validation, and Cleaning 99 | - If validation raises an Error, cleaning is skipped 100 | - Validators are callables that can raise a ``ValidationError`` 101 | - Django includes generic ones for some common tasks 102 | - Examples: URL, Min/Max Value, Min/Max Length, URL, Regex, email 103 | 104 | .. slide:: Field Cleaning 105 | :level: 2 106 | 107 | - ``.clean_fieldname()`` method is called after validators 108 | - Input has already been converted to Python objects 109 | - Methods can raise ``ValidationErrors`` 110 | - Methods *must* return the cleaned value 111 | 112 | Up until this point we've been using forms without really needing to 113 | be aware of it. A `Django Form`_ is responsible for taking some user 114 | input, validating it, and turning it into Python objects. They also 115 | have some handy rendering methods, but I consider those sugar: the 116 | real power is in making sure that input from your users is what it 117 | says it is. 118 | 119 | The `Generic Views`_, specifically the ones we've been using, all 120 | operate on a particular model. Django is able to take the model 121 | definition that we've created and extrapolate a Form from it. Django 122 | can do this because both Models and Forms are constructed of fields 123 | that have a particular type and particular validation rules. Models 124 | use those fields to map data to types that your database understands; 125 | Forms use them to map input to Python types [1]_. Forms that map to a 126 | particular Model are called ModelForms_; you can think of them as 127 | taking user input and transforming it into an instance of a Model. 128 | 129 | .. [1] While I'm referring to them both as fields, they're really 130 | completely different implementations. But the analogy holds. 131 | 132 | Adding Fields to the Form 133 | ------------------------- 134 | 135 | .. checkpoint:: confirm_contact_email 136 | 137 | So what if we want to add a field to our form? Say, we want to require 138 | confirmation of the email address. In that case we can create a new 139 | form, and override the default used by our views. 140 | 141 | First, in the ``contacts`` app directory, we'll create a new file, 142 | ``forms.py``. 143 | 144 | .. literalinclude:: /src/contacts/forms.py 145 | :end-before: def clean 146 | 147 | Here we're creating a new ``ModelForm``; we associate the form with 148 | our model in the ``Meta`` inner class. 149 | 150 | We're also adding an additional field, ``confirm_email``. This is an 151 | example of a field declaration in a model. The first argument is the 152 | label, and then there are additional keyword arguments; in this case, 153 | we simply mark it required. 154 | 155 | Finally, in the constructor we mutate the ``initial`` kwarg. 156 | ``initial`` is a dictionary of values that will be used as the default 157 | values for an `unbound form`_. Model Forms have another kwarg, 158 | ``instance``, that holds the instance we're editing. 159 | 160 | Overriding the Default Form 161 | --------------------------- 162 | 163 | We've defined a form with the extra field, but we still need to tell 164 | our view to use it. You can do this in a couple of ways, but the 165 | simplest is to set the ``form_class`` property on the View class. 166 | We'll add that property to our ``CreateContactView`` and 167 | ``UpdateContactView`` in ``views.py``. 168 | 169 | .. literalinclude:: /src/contacts/views.py 170 | :prepend: import forms 171 | ... 172 | :pyobject: CreateContactView 173 | :end-before: def get_success_url 174 | 175 | .. literalinclude:: /src/contacts/views.py 176 | :pyobject: UpdateContactView 177 | :end-before: def get_success_url 178 | 179 | If we fire up the server and visit the edit or create pages, we'll see 180 | the additional field. We can see that it's required, but there's no 181 | validation that the two fields match. To support that we'll need to 182 | add some custom validation to the Form. 183 | 184 | Customizing Validation 185 | ---------------------- 186 | 187 | Forms have two different phases of validation: field and form. All the 188 | fields are validated and converted to Python objects (if possible) 189 | before form validation begins. 190 | 191 | Field validation takes place for an individual field: things like 192 | minimum and maximum length, making sure it looks like a URL, and date 193 | range validation are all examples of field validation. Django doesn't 194 | guarantee that field validation happens in any order, so you can't 195 | count on other fields being available for comparison during this 196 | phase. 197 | 198 | Form validation, on the other hand, happens after all fields have been 199 | validated and converted to Python objects, and gives you the 200 | opportunity to do things like make sure passwords match, or in this 201 | case, email addresses. 202 | 203 | Form validation takes place in a form's ``clean()`` method. 204 | 205 | .. literalinclude:: /src/contacts/forms.py 206 | :prepend: class ContactForm(forms.ModelForm): 207 | ... 208 | :language: python 209 | :pyobject: ContactForm.clean 210 | :end-before: inlineformset 211 | 212 | When you enter the ``clean`` method, all of the fields that validated 213 | are available in the ``cleaned_data`` dictionary. The ``clean`` method 214 | may add, remove, or modify values, but **must** return the dictionary 215 | of cleaned data. ``clean`` may also raise a ``ValidationError`` if it 216 | encounters an error. This will be available as part of the forms' 217 | ``errors`` property, and is shown by default when you render the form. 218 | 219 | Note that I said ``cleaned_data`` contains all the fields *that 220 | validated*. That's because form-level validation **always** happens, 221 | even if no fields were successfully validated. That's why in the clean 222 | method we use ``cleaned_data.get('email')`` instead of 223 | ``cleaned_data['email']``. 224 | 225 | If you visit the create or update views now, we'll see an extra field 226 | there. Try to make a change, or create a contact, without entering the 227 | email address twice. 228 | 229 | Controlling Form Rendering 230 | -------------------------- 231 | 232 | .. checkpoint:: custom_form_rendering 233 | 234 | .. slide:: Rendering Forms 235 | :level: 2 236 | 237 | Three primary "whole-form" output modes: 238 | 239 | - ``form.as_p()``, ``form.as_ul()``, ``form.as_table()`` 240 | 241 | :: 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | Our templates until now look pretty magical when it comes to forms: 251 | the extent of our HTML tags has been something like:: 252 | 253 |
    254 | {% csrf_token %} 255 |
      256 | {{ form.as_ul }} 257 |
    258 | 259 |
    260 | 261 | We're living at the whim of ``form.as_ul``, and it's likely we want 262 | something different. 263 | 264 | Forms have three pre-baked output formats: ``as_ul``, ``as_p``, and 265 | ``as_table``. If ``as_ul`` outputs the form elements as the items in 266 | an unordered list, it's not too mysterious what ``as_p`` and 267 | ``as_table`` do. 268 | 269 | .. slide:: Controlling Form Output 270 | :level: 2 271 | 272 | :: 273 | 274 | {% for field in form %} 275 | {{ field.label_tag }}: {{ field }} 276 | {{ field.errors }} 277 | {% endfor %} 278 | {{ form.non_field_errors }} 279 | 280 | Additional rendering properties: 281 | 282 | - ``field.label`` 283 | - ``field.label_tag`` 284 | - ``field.auto_id`` 285 | - ``field.help_text`` 286 | 287 | Often, though, you need more control. For those cases, you can take 288 | full control. First, a form is iterable; try replacing your call to 289 | ``{{form.as_ul}}`` with this:: 290 | 291 | {% for field in form %} 292 | {{ field }} 293 | {% endfor %} 294 | 295 | As you can see, ``field`` renders as the input for each field in the 296 | form. When you iterate over a Form, you're iterating over a sequence 297 | of `BoundField`_ objects. A ``BoundField`` wraps the field definition 298 | from your Form (or derived from the ModelForm) along with any data and 299 | error state it may be bound to. This means it has some properties that 300 | are handy for customizing rendering. 301 | 302 | In addition to supporting iteration, you can access an individual 303 | BoundField directly, treating the Form like a dictionary:: 304 | 305 | {{ form.email }} 306 | 307 | .. sidebar:: Dictionary!?! 308 | 309 | That may not look like a dictionary access, but remember that Django 310 | templates are quite restrictive in their syntax. Writing ``foo.bar`` 311 | will look for a property ``bar`` on ``foo``, and if it's callable, 312 | call it. If it doesn't find a property, it'll map that to something 313 | like ``foo['bar']``. So when it comes to writing Django templates, 314 | dictionary elements act just like properties. 315 | 316 | Consider the following alternative to ``edit_contact.html``. 317 | 318 | .. literalinclude:: /src/contacts/templates/edit_contact_custom.html 319 | :language: html 320 | 321 | In this example we see a few different things at work: 322 | 323 | * ``field.auto_id`` to get the automatically generated field ID 324 | * Combining that ID with ``_container`` and ``_errors`` to give our 325 | related elements names that consistently match 326 | * Using ``field.label_tag`` to generate the label. ``label_tag`` adds 327 | the appropriate ``for`` property to the tag, too. For the 328 | ``last_name`` field, this looks like:: 329 | 330 | 331 | 332 | * Using ``field.errors`` to show the errors in a specific place. The 333 | Django Form documentation has details on further customizing `how 334 | errors are displayed`_. 335 | * Finally, ``field.help_text``. You can specify a ``help_text`` 336 | keyword argument to each field when creating your form, which is 337 | accessible here. Defining that text in the Form definition is 338 | desirable because you can easily mark it up for translation. 339 | 340 | Testing Forms 341 | ------------- 342 | 343 | .. checkpoint:: contact_form_test 344 | 345 | It's easy to imagine how you'd use the ``LiveServerTestCase`` to write 346 | an integration test for a Form. But that wouldn't just be testing the 347 | Form, that'd be testing the View, the URL configuration, and probably 348 | the Model (in this case, at least). We've built some custom logic into 349 | our form's validator, and it's important to test that and that alone. 350 | Integration tests are invaluable, but when they fail there's more than 351 | one suspect. I like tests that fail with a single suspect. 352 | 353 | Writing unit tests for a Form usually means crafting some dictionary 354 | of form data that meets the starting condition for your test. Some 355 | Forms can be complex or long, so we can use a helper to generate the 356 | starting point from the Form's initial data. 357 | 358 | **Rebar** is a collection of utilities for working with Forms. We'll 359 | install Rebar so we can use the testing utilities. 360 | 361 | :: 362 | 363 | (tutorial)$ pip install rebar 364 | 365 | Then we can write a unit test that tests two cases: success (email 366 | addresses match) and failure (they do not). 367 | 368 | .. literalinclude:: /src/contacts/tests.py 369 | :prepend: from rebar.testing import flatten_to_dict 370 | from contacts import forms 371 | ... 372 | :pyobject: EditContactFormTests 373 | 374 | An interesting thing to note here is the use of the ``is_valid()`` 375 | method. We could just as easily introspect the ``errors`` property 376 | that we used in our template above, but in this case we just need a 377 | Boolean answer: is the form valid, or not? Note that we do need to 378 | provide a first and last name, as well, since those are required 379 | fields. 380 | 381 | Review 382 | ------ 383 | 384 | * Forms take user input, validate it, and convert it to Python objects 385 | * Forms are composed of Fields, just like Models 386 | * Fields have validation built in 387 | * You can customize per-field validation, as well as form validation 388 | * If you need to compare fields to one another, you need to implement 389 | the ``clean`` method 390 | * Forms are iterable over, and support dictionary-like access to, the 391 | bound fields 392 | * A Bound Field has properties and methods for performing fine-grained 393 | customization of rendering. 394 | * Forms are unit testable; Rebar has some utilities to help with 395 | testing large forms. 396 | 397 | .. _`Django Form`: https://docs.djangoproject.com/en/1.5/topics/forms/ 398 | .. _`Generic Views`: https://docs.djangoproject.com/en/1.5/topics/class-based-views/ 399 | .. _ModelForms: https://docs.djangoproject.com/en/1.5/topics/forms/modelforms 400 | .. _`unbound form`: https://docs.djangoproject.com/en/1.5/ref/forms/api/#ref-forms-api-bound-unbound 401 | .. _`BoundField`: https://docs.djangoproject.com/en/1.5/ref/forms/api/#django.forms.BoundField 402 | .. _`how errors are displayed`: https://docs.djangoproject.com/en/1.5/ref/forms/api/#how-errors-are-displayed 403 | -------------------------------------------------------------------------------- /tutorial/getting-started.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | .. slideconf:: 5 | :autoslides: True 6 | :theme: slides 7 | 8 | ================= 9 | Getting Started 10 | ================= 11 | 12 | Your Development Environment 13 | ============================ 14 | 15 | .. slide:: The Environment 16 | :level: 3 17 | 18 | Three important factors for your environment: 19 | 20 | * Isolation 21 | * Determinism 22 | * Similarity 23 | 24 | .. ifnotslides:: 25 | 26 | When thinking about your development environment, there are three 27 | important things to keep in mind: isolation, determinism, and 28 | similarity. They're each important, and they work in concert with one 29 | another. 30 | 31 | **Isolation** means that you're not inadvertently leveraging tools 32 | or packages installed outside the environment. This is particularly 33 | important when it comes to something like Python packages with C 34 | extensions: if you're using something installed at the system level 35 | and don't know it, you can find that when you go to deploy or share 36 | your code that it doesn't operate the way you expect. A tool like 37 | virtualenv_ can help create that sort of environment. 38 | 39 | Your environment is **deterministic** if you're confident about 40 | what versions of your dependencies you're relying on, and can 41 | reproduce that environment reliably. 42 | 43 | Finally, **similarity** to your production or deployment 44 | environment means you're running on the same OS, preferably the 45 | same release, and that you're using the same tools to configure 46 | your development environment that you use to configure your 47 | deployment environment. This is by no means a requirement, but as 48 | you build bigger, more complex software, it's helpful to be 49 | confident that any problem you see in production is reproducable in 50 | your development environment, and limit the scope of investigation 51 | to code you wrote. 52 | 53 | .. _virtualenv: http://www.virtualenv.org/ 54 | 55 | Isolation 56 | --------- 57 | 58 | * We want to avoid using unknown dependencies, or unknown versions 59 | * virtualenv_ provides an easy way to work on a project without your 60 | system's ``site-packages`` 61 | 62 | Determinism 63 | ----------- 64 | 65 | * Determinism is all about dependency management 66 | * Choose a tool, use it in development and production 67 | 68 | * pip, specifically a `requirements files`_ 69 | * buildout_ 70 | * install_requires_ in setup.py 71 | 72 | * Identify specific versions of dependencies 73 | 74 | .. ifnotslides:: 75 | 76 | You can specify versions either by the version for a package on 77 | PyPI, or a specific revision (SHA in git, revision number in 78 | Subversion, etc). This ensures that you're getting the exact 79 | version you're testing with. 80 | 81 | .. _`requirements files`: http://www.pip-installer.org/en/latest/requirements.html 82 | .. _buildout: http://www.buildout.org/ 83 | .. _install_requires: http://pythonhosted.org/distribute/setuptools.html#declaring-dependencies 84 | 85 | Similarity 86 | ---------- 87 | 88 | * Working in an environment similar to where you deploy eliminates 89 | variables when trying to diagnose an issue 90 | * If you're building something that requires additional services, this 91 | becomes even more important. 92 | * Vagrant_ is a tool for managing virtual machines, lets you easily 93 | create an environment separate from your day to day work. 94 | 95 | .. _Vagrant: http://vagrantup.com/ 96 | 97 | 98 | Setting Up Your Environment 99 | =========================== 100 | 101 | .. checkpoint:: environment 102 | 103 | Create a Clean Workspace 104 | ------------------------ 105 | 106 | :: 107 | 108 | $ mkdir tutorial 109 | $ virtualenv ./tutorial/ 110 | New python executable in ./tutorial/bin/python 111 | Installing setuptools............done. 112 | Installing pip...............done. 113 | $ source ./tutorial/bin/activate 114 | (tutorial)$ 115 | 116 | .. Alternately, start by cloning the `example repository`_:: 117 | 118 | .. $ git clone git://github.com/nyergler/effective-django-tutorial.git 119 | .. $ cd effective-django-tutorial 120 | .. $ git checkout environment 121 | .. $ virtualenv . 122 | .. New python executable in ./bin/python 123 | .. Installing setuptools............done. 124 | .. Installing pip...............done. 125 | .. $ source ./bin/activate 126 | .. (effective-django-tutorial) $ 127 | 128 | .. _`example repository`: https://github.com/nyergler/effective-django-tutorial 129 | 130 | Start a Requirements File 131 | ------------------------- 132 | 133 | Create a ``requirements.txt`` in the ``tutorial`` directory with a 134 | single requirement in it. 135 | 136 | .. literalinclude:: /src/requirements.txt 137 | 138 | Installing Requirements 139 | ----------------------- 140 | 141 | And then we can use pip_ to install the dependencies. 142 | 143 | :: 144 | 145 | (tutorial)$ pip install -U -r requirements.txt 146 | 147 | Downloading/unpacking Django==1.5.1 148 | Downloading Django-1.5.1.tar.gz (8.0MB): 8.0MB downloaded 149 | Running setup.py egg_info for package Django 150 | 151 | warning: no previously-included files matching '__pycache__' found under directory '*' 152 | warning: no previously-included files matching '*.py[co]' found under directory '*' 153 | Installing collected packages: Django 154 | Running setup.py install for Django 155 | changing mode of build/scripts-2.7/django-admin.py from 644 to 755 156 | 157 | warning: no previously-included files matching '__pycache__' found under directory '*' 158 | warning: no previously-included files matching '*.py[co]' found under directory '*' 159 | changing mode of /home/nathan/p/edt/bin/django-admin.py to 755 160 | Successfully installed Django 161 | Cleaning up... 162 | 163 | .. _pip: http://www.pip-installer.org/ 164 | 165 | Beginning a Django Project 166 | ========================== 167 | 168 | .. ifnotslides:: 169 | 170 | When a building is under construction, scaffolding is often used to 171 | support the structure before it's complete. The scaffolding can be 172 | temporary, or it can serve as part of the foundation for the 173 | building, but regardless it provides some support when you're just 174 | starting out. 175 | 176 | Django, like many web frameworks, provides scaffolding for your 177 | development efforts. It does this by making decisions and providing 178 | a starting point for your code that lets you focus on the problem 179 | you're trying to solve, and not how to parse an HTTP request. 180 | Django provides HTTP, as well as file system scaffolding. 181 | 182 | The HTTP scaffolding handles things like parsing an HTTP request 183 | into a Python object and providing tools to easily create a 184 | response. The file system scaffolding is a little different: it's a 185 | set of conventions for organizing your code. These conventions make 186 | it easier to add engineers to a project, since they 187 | (hypothetically) have some idea how the code is organized. In 188 | Django parlance, a **project** is the final product, and it 189 | assembles one or more **applications** together. Django 1.4 made a 190 | change to the way the `projects and applications are laid out on 191 | disk`_, which makes it easier to decouple and reuse applications 192 | between projects. 193 | 194 | .. _`projects and applications are laid out on disk`: https://docs.djangoproject.com/en/1.5/releases/1.4/#updated-default-project-layout-and-manage-py 195 | 196 | .. slide:: Scaffolding 197 | :level: 3 198 | 199 | * Django provides file system scaffolding just like HTTP scaffolding 200 | * Helps engineers understand where to find things when they go looking 201 | * Django 1.4 made a change that decouples apps from projects 202 | * In Django parlance, your **project** is a collection of 203 | **applications**. 204 | 205 | Creating the Project 206 | -------------------- 207 | 208 | .. ifnotslides:: 209 | 210 | Django installs a ``django-admin.py`` script for handling scaffolding 211 | tasks. We'll use ``startproject`` to create the project files. We 212 | specify the project name and the directory to start in; we're already 213 | in our isolated environment so we can just say ``.`` 214 | 215 | :: 216 | 217 | (tutorial)$ django-admin.py startproject addressbook . 218 | 219 | :: 220 | 221 | manage.py 222 | ./addressbook 223 | __init__.py 224 | settings.py 225 | urls.py 226 | wsgi.py 227 | 228 | Project Scaffolding 229 | ------------------- 230 | 231 | .. ifnotslides:: 232 | 233 | * ``manage.py`` is a pointer back to ``django-admin.py`` with an 234 | environment variable set, pointing to your project as the one to 235 | read settings from and operate on when needed. 236 | * ``settings.py`` is where you'll configure your project. It has a 237 | few sensible defaults, but no database chosen when you start. 238 | * ``urls.py`` contains the URL to view mappings: we'll talk more about 239 | that shortly. 240 | * ``wsgi.py`` is a WSGI_ wrapper for your application. This is used 241 | by Django's development servers, and possibly other containers 242 | like mod_wsgi, uwsgi, etc. in production. 243 | 244 | .. ifslides:: 245 | 246 | manage.py 247 | Wrapper around ``django-admin.py`` that operates on your project. You 248 | can run the tests or the development server using this. 249 | 250 | settings.py 251 | Your project configuration. 252 | 253 | urls.py 254 | URL definitions for your project 255 | 256 | wsgi.py 257 | A wrapper for running your project in a WSGI_ server. 258 | 259 | .. _WSGI: https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface 260 | 261 | Creating the "App" 262 | ------------------ 263 | 264 | :: 265 | 266 | (tutorial)$ python ./manage.py startapp contacts 267 | 268 | :: 269 | 270 | ./addressbook 271 | ./contacts 272 | __init__.py 273 | models.py 274 | tests.py 275 | views.py 276 | 277 | .. notslides:: 278 | 279 | * Beginning in Django 1.4, *apps* are placed alongside *project* 280 | packages. This is a great improvement when it comes to 281 | deployment. 282 | * ``models.py`` will contain the Django ORM models for your app. 283 | * ``views.py`` will contain the View code 284 | * ``tests.py`` will contain the unit and integration tests you 285 | write. 286 | 287 | .. XXX: I wish this weren't so stupid; maybe this doc should just have 288 | .. autoslides turned off? 289 | 290 | .. ifnotslides:: 291 | 292 | Review 293 | ====== 294 | 295 | * Make sure your development environment is deterministic and as 296 | similar to where you'll deploy as possible 297 | * Specify explicit versions for your dependencies 298 | * Django organizes code into "Projects" and "Applications" 299 | * Applications are [potentially] reusable 300 | 301 | .. slide:: Review 302 | :level: 3 303 | 304 | * Make sure your development environment is deterministic and as 305 | similar to where you'll deploy as possible 306 | * Specify explicit versions for your dependencies 307 | * Django organizes code into "Projects" and "Applications" 308 | * Applications are [potentially] reusable 309 | 310 | * Next: :doc:`models` 311 | -------------------------------------------------------------------------------- /tutorial/index.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: ./src 3 | 4 | .. slideconf:: 5 | :autoslides: False 6 | :theme: single-level 7 | 8 | =========================== 9 | Effective Django Tutorial 10 | =========================== 11 | 12 | .. note:: 13 | 14 | `Video of this tutorial`_ from PyCon is available on YouTube. 15 | 16 | .. _`Video of this tutorial`: https://www.youtube.com/watch?v=NfsJDPm0X54 17 | 18 | Django is a popular, powerful web framework for Python. It has lots of 19 | "batteries" included, and makes it easy to get up and going. But all 20 | of the power means you can write low quality code that still seems to 21 | work. So what does *Effective Django* mean? It means using Django in a 22 | way that emphasizes writing code that's cohesive, testable, and 23 | scalable. What do each of those words mean? 24 | 25 | Well, "cohesive" code is code that is focused on doing one thing, and 26 | one thing alone. It means that when you write a function or a method, 27 | that it does one thing and does it well. 28 | 29 | This is directly related to writing testable code: code that's doing 30 | too much is often difficult to write tests for. When I find myself 31 | thinking, "Well, this piece of code is just too complex to write a 32 | test for, it's not really worth all the effort," that's a signal that 33 | I need to step back and focus on simplifying it. Testable code is code 34 | that makes it straight-forward to write tests for, and that's easy to 35 | diagnose problems with. 36 | 37 | Finally, we want to write scalable code. That doesn't just mean it 38 | scales in terms of performance, but that it also scales in terms of 39 | your team and your team's understanding. Applications that are well 40 | tested are easier for others to understand (and easier for them to 41 | modify), which means you're more able to improve your application by 42 | adding engineers. 43 | 44 | My goal is to convince you of the importance of these principles, and 45 | provide examples of how to follow them to build more robust Django 46 | applications. I'm going to walk through building a contact management 47 | application iteratively, talking about the choices and testing 48 | strategy as I go. 49 | 50 | The sample code for this tutorial is available in the 51 | `effective-django-tutorial`_ git repository. 52 | 53 | Slides for the tutorial are available at http://effectivedjango.com/slides/tutorial 54 | 55 | .. _`effective-django-tutorial`: https://github.com/nyergler/effective-django-tutorial/ 56 | 57 | .. slide:: Effective Django 58 | :level: 1 59 | 60 | .. figure:: /_static/building.jpg 61 | :class: fill 62 | 63 | CC BY-NC-SA http://www.flickr.com/photos/t_lawrie/278932896/ 64 | 65 | http://effectivedjango.com 66 | 67 | Nathan Yergler // nathan@yergler.net // @nyergler 68 | 69 | .. slide:: Goals 70 | :level: 2 71 | 72 | * Build a small Django application 73 | * Build it effectively 74 | * Explore some of the new features in Django 75 | 76 | .. slide:: Effective? 77 | :level: 2 78 | 79 | * Cohesive 80 | * Tested 81 | * Scalable 82 | 83 | .. ifnotslides:: 84 | 85 | .. toctree:: 86 | :maxdepth: 1 87 | 88 | getting-started.rst 89 | models.rst 90 | views.rst 91 | static.rst 92 | additional-views.rst 93 | forms.rst 94 | related.rst 95 | authzn.rst 96 | 97 | "Effective Django" is licensed under the Creative Commons 98 | `Attribution-ShareAlike 4.0 International License`_. 99 | 100 | .. _`Attribution-ShareAlike 4.0 International License`: http://creativecommons.org/licenses/by-sa/4.0/ 101 | -------------------------------------------------------------------------------- /tutorial/models.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | .. slideconf:: 5 | :autoslides: False 6 | :theme: single-level 7 | 8 | ============= 9 | Using Models 10 | ============= 11 | 12 | .. slide:: Django Models 13 | :level: 1 14 | 15 | Storing and manipulating data with the Django ORM. 16 | 17 | 18 | .. checkpoint:: contact_model 19 | 20 | Configuring the Database 21 | ======================== 22 | 23 | .. slide:: Configuring Databases 24 | :level: 2 25 | 26 | .. literalinclude:: /src/addressbook/settings.py 27 | :language: python 28 | :lines: 12-21 29 | 30 | Django includes support out of the box for MySQL, PostgreSQL, SQLite3, 31 | and Oracle. SQLite3_ is included with Python starting with version 32 | 2.5, so we'll use it for our project for simplicity. If you were going 33 | to use MySQL, for example, you'd need to add `mysql-python`_ to your 34 | ``requirements.txt`` file. 35 | 36 | To enable SQLite as the database, edit the ``DATABASES`` definition in 37 | ``addressbook/settings.py``. The ``settings.py`` file contains the 38 | Django configuration for our project. There are some settings that you 39 | must specify -- like the ``DATABASES`` configuration -- and others 40 | that are optional. Django fills in some defaults when it generates the 41 | project scaffolding, and the documentation contains a `full list of 42 | settings`_. You can also add your own settings here, if needed. 43 | 44 | For SQLite we need to set the engine and then give it a name. The 45 | SQLite backend uses the ``NAME`` as the filename for the database. 46 | 47 | .. literalinclude:: /src/addressbook/settings.py 48 | :language: python 49 | :lines: 12-21 50 | 51 | Note that the database engine is specified as a string, and not a 52 | direct reference to the Python object. This is because the settings 53 | file needs to be easily importable, without triggering any side 54 | effects. You should avoid adding imports to the settings file. 55 | 56 | You rarely need to import the settings file directly; Django imports 57 | it for you, and makes it available as ``django.conf.settings``. You 58 | typically import your settings from ``django.conf``:: 59 | 60 | from django.conf import settings 61 | 62 | Creating a Model 63 | ================ 64 | 65 | .. slide:: Defining Models 66 | :level: 2 67 | 68 | Models are created in the ``models`` module of a Django app and 69 | subclass Model_ 70 | 71 | .. literalinclude:: /src/contacts/models.py 72 | :language: python 73 | :end-before: __str__ 74 | 75 | Django models map (roughly) to a database table, and provide a place 76 | to encapsulate business logic. All models subclass the base Model_ 77 | class, and contain field definitions. Let's start by creating a simple 78 | Contact model for our application in ``contacts/models.py``. 79 | 80 | 81 | .. literalinclude:: /src/contacts/models.py 82 | :language: python 83 | 84 | Django provides a set of fields_ that map to data types and different 85 | validation rules. For example, the ``EmailField`` here maps to the 86 | same column type as the ``CharField``, but adds validation for the 87 | data. 88 | 89 | Once you've created a model, you need to update your database with the 90 | new tables. Django's ``syncdb`` command looks for models that are 91 | installed and creates tables for them if needed. 92 | 93 | :: 94 | 95 | (tutorial)$ python ./manage.py syncdb 96 | 97 | Creating tables ... 98 | Creating table auth_permission 99 | Creating table auth_group_permissions 100 | Creating table auth_group 101 | Creating table auth_user_user_permissions 102 | Creating table auth_user_groups 103 | Creating table auth_user 104 | Creating table django_content_type 105 | Creating table django_session 106 | Creating table django_site 107 | 108 | ... 109 | 110 | Our contact table isn't anywhere to be seen. The reason is that we 111 | need to tell the Project to use the Application. 112 | 113 | The ``INSTALLED_APPS`` setting lists the applications that the project 114 | uses. These are listed as strings that map to Python packages. Django 115 | will import each and looks for a ``models`` module there. Add our 116 | Contacts app to the project's ``INSTALLED_APPS`` setting: 117 | 118 | .. literalinclude:: /src/addressbook/settings.py 119 | :language: python 120 | :lines: 111-123 121 | 122 | Then run ``syncdb`` again:: 123 | 124 | (tutorial)$ python ./manage.py syncdb 125 | Creating tables ... 126 | Creating table contacts_contact 127 | Installing custom SQL ... 128 | Installing indexes ... 129 | Installed 0 object(s) from 0 fixture(s) 130 | 131 | Note that Django created a table named ``contacts_contact``: by 132 | default Django will name your tables using a combination of the 133 | application name and model name. You can override that with the 134 | `model Meta`_ options. 135 | 136 | 137 | Interacting with the Model 138 | ========================== 139 | 140 | .. slide:: Instantiating Models 141 | :level: 2 142 | 143 | :: 144 | 145 | nathan = Contact() 146 | nathan.first_name = 'Nathan' 147 | nathan.last_name = 'Yergler' 148 | nathan.save() 149 | 150 | :: 151 | 152 | nathan = Contact.objects.create( 153 | first_name='Nathan', 154 | last_name='Yergler') 155 | 156 | :: 157 | 158 | nathan = Contact( 159 | first_name='Nathan', 160 | last_name='Yergler') 161 | nathan.save() 162 | 163 | Now that the model has been synced to the database we can interact 164 | with it using the interactive shell. 165 | 166 | :: 167 | 168 | (tutorial)$ python ./manage.py shell 169 | Python 2.7.3 (default, Aug 9 2012, 17:23:57) 170 | [GCC 4.7.1 20120720 (Red Hat 4.7.1-5)] on linux2 171 | Type "help", "copyright", "credits" or "license" for more information. 172 | (InteractiveConsole) 173 | >>> from contacts.models import Contact 174 | >>> Contact.objects.all() 175 | [] 176 | >>> Contact.objects.create(first_name='Nathan', last_name='Yergler') 177 | 178 | >>> Contact.objects.all() 179 | [] 180 | >>> nathan = Contact.objects.get(first_name='Nathan') 181 | >>> nathan 182 | 183 | >>> print nathan 184 | Nathan Yergler 185 | >>> nathan.id 186 | 1 187 | 188 | There are a few new things here. First, the ``manage.py shell`` 189 | command gives us a interactive shell with the Python path set up 190 | correctly for Django. If you try to run Python and just import your 191 | application, an Exception will be raised because Django doesn't know 192 | which ``settings`` to use, and therefore can't map Model instances to 193 | the database. 194 | 195 | Second, there's this ``objects`` property on our model class. That's 196 | the model's Manager_. If a single instance of a Model is analogous to 197 | a row in the database, the Manager is analogous to the table. The 198 | default model manager provides querying functionality, and can be 199 | customized. When we call ``all()`` or ``filter()`` or the Manager, a 200 | QuerySet is returned. A QuerySet is iterable, and loads data from the 201 | database as needed. 202 | 203 | Finally, there's this ``id`` field that we didn't define. Django adds 204 | an ``id`` field as the primary key for your model, unless you `specify 205 | a primary key`_. 206 | 207 | 208 | .. slide:: Model Managers 209 | :level: 2 210 | 211 | * A model instance maps to a row 212 | * The model Manager_ maps to the table 213 | * Every model has a default manager, ``objects`` 214 | * Operations that deal with more than one instance, or at the 215 | "collection" level, usually map to the Manager 216 | 217 | .. slide:: Querying with Managers 218 | :level: 2 219 | 220 | * The ``filter`` Manager method lets you perform queries:: 221 | 222 | Contact.objects.filter(last_name='Yergler') 223 | 224 | * ``filter`` returns a QuerySet_, an iterable over the result. 225 | * You can also assert you only expect one:: 226 | 227 | Contact.objects.get(first_name='Nathan') 228 | 229 | * If more than one is returned, an Exception will be raised 230 | * The full query_ reference is pretty good on this topic. 231 | 232 | Writing a Test 233 | ============== 234 | 235 | .. slide:: Testing Models 236 | :level: 2 237 | 238 | * Business logic is usually added as methods on a Model. 239 | * Important to write unit tests for those methods as you add them. 240 | * We'll write an example test for the methods we add. 241 | 242 | 243 | We have one method defined on our model, ``__str__``, and this is a 244 | good time to start writing tests. The ``__str__`` method of a model 245 | will get used in quite a few places, and it's entirely conceivable 246 | it'd be exposed to end users. It's worth writing a test so we 247 | understand how we expect it to operate. Django creates a ``tests.py`` 248 | file when it creates the application, so we'll add our first test to 249 | that file in the contacts app. 250 | 251 | .. literalinclude:: /src/contacts/tests.py 252 | :language: python 253 | :prepend: from contacts.models import Contact 254 | ... 255 | :pyobject: ContactTests 256 | 257 | .. slide:: Running the Tests 258 | :level: 2 259 | 260 | You can run the tests for your application using ``manage.py``:: 261 | 262 | (tutorial)$ python manage.py test 263 | 264 | 265 | You can run the tests for your application using ``manage.py``:: 266 | 267 | (tutorial)$ python manage.py test 268 | 269 | If you run this now, you'll see that around 420 tests run. That's 270 | surprising, since we've only written one. That's because by default 271 | Django runs the tests for all installed applications. When we added 272 | the ``contacts`` app to our project, there were several Django apps 273 | there by default. The extra 419 tests come from those. 274 | 275 | If you want to run the tests for a specific app, just specify the app 276 | name on the command line:: 277 | 278 | (tutorial)$ python manage.py test contacts 279 | Creating test database for alias 'default'... 280 | .. 281 | ---------------------------------------------------------------------- 282 | Ran 2 tests in 0.000s 283 | 284 | OK 285 | Destroying test database for alias 'default'... 286 | $ 287 | 288 | One other interesting thing to note before moving on is the first and 289 | last line of output: "Creating test database" and "Destroying test 290 | database". Some tests need access to a database, and because we don't 291 | want to mingle test data with "real" data (for a variety of reasons, 292 | not the least of which is determinism), Django helpfully creates a 293 | test database for us before running the tests. Essentially it creates 294 | a new database, and runs ``syncdb`` on it. If you subclass from 295 | Django's ``TestCase`` (which we are), Django also resets any default 296 | data after running each TestCase, so that changes in one test won't 297 | break or influence another. 298 | 299 | .. rst-class:: include-as-slide, slide-level-2 300 | 301 | Review 302 | ====== 303 | 304 | * Models define the fields in a table, and can contain business logic. 305 | * The ``syncdb`` manage command creates the tables in your database from 306 | models 307 | * The model Manager allows you to operate on the collection of 308 | instances: querying, creating, etc. 309 | * Write unit tests for methods you add to the model 310 | * The ``test`` manage command runs the unit tests 311 | 312 | .. ifslides:: 313 | 314 | * Next: :doc:`views` 315 | 316 | .. _QuerySet: https://docs.djangoproject.com/en/1.5/ref/models/querysets/#django.db.models.query.QuerySet 317 | .. _query: https://docs.djangoproject.com/en/1.5/topics/db/queries/ 318 | .. _SQLite3: http://docs.python.org/2/library/sqlite3.html 319 | .. _mysql-python: https://pypi.python.org/pypi/MySQL-python 320 | .. _`full list of settings`: https://docs.djangoproject.com/en/1.5/ref/settings/ 321 | .. _Model: https://docs.djangoproject.com/en/1.5/ref/models/instances/#django.db.models.Model 322 | .. _Manager: https://docs.djangoproject.com/en/1.5/topics/db/managers/ 323 | .. _`specify a primary key`: https://docs.djangoproject.com/en/1.5/topics/db/models/#automatic-primary-key-fields 324 | .. _fields: https://docs.djangoproject.com/en/1.5/ref/models/fields/ 325 | .. _`model Meta`: https://docs.djangoproject.com/en/1.5/ref/models/options/ 326 | -------------------------------------------------------------------------------- /tutorial/related.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | ============== 5 | Related Models 6 | ============== 7 | 8 | Adding Relationships 9 | ==================== 10 | 11 | .. checkpoint:: address_model 12 | 13 | We have a basic email address book at this point, but there's other 14 | information we might want to track for our contacts. Mailing 15 | addresses, for example. A single Contact may have multiple addresses 16 | associated with them, so we'll store this in a separate table, 17 | allowing us to have multiple addresses for each Contact. 18 | 19 | .. literalinclude:: /src/contacts/models.py 20 | :pyobject: Address 21 | 22 | Django provides three types of fields for relating objects to each 23 | other: ``ForeignKey`` for creating one to many relationships, 24 | ``ManyToManyField`` for relating many to many, and ``OneToOneField`` 25 | for creating a one to one relationship. You define the relationship in 26 | one model, but it's accessible from the other side, as well. 27 | 28 | Sync up the database to create the table, and then start the shell so 29 | we can explore this. 30 | 31 | :: 32 | 33 | (tutorial)$ python manage.py syncdb 34 | Creating tables ... 35 | Creating table contacts_address 36 | Installing custom SQL ... 37 | Installing indexes ... 38 | Installed 0 object(s) from 0 fixture(s) 39 | 40 | Now that we have the model created, we can again play with it using 41 | the interactive shell. 42 | 43 | :: 44 | 45 | (tutorial)$ python manage.py shell 46 | Python 2.7.3 (default, Aug 9 2012, 17:23:57) 47 | [GCC 4.7.1 20120720 (Red Hat 4.7.1-5)] on linux2 48 | Type "help", "copyright", "credits" or "license" for more information. 49 | (InteractiveConsole) 50 | >>> from contacts.models import Contact, Address 51 | >>> nathan = Contact.objects.create(first_name='Nathan', email='nathan@yergler.net') 52 | >>> nathan.address_set.all() 53 | [] 54 | >>> nathan.address_set.create(address_type='home', 55 | ... city='San Francisco', state='CA', postal_code='94107') 56 | 57 | >>> nathan.address_set.create(address_type='college', 58 | ... address='354 S. Grant St.', city='West Lafayette', state='IN', 59 | ... postal_code='47906') 60 | 61 | >>> nathan.address_set.all() 62 | [, ] 63 | >>> nathan.address_set.filter(address_type='college') 64 | 65 | >>> Address.objects.filter(contact__first_name='Nathan') 66 | [, ] 67 | 68 | As you can see, even though we defined the relationship between 69 | Contacts and Addresses on the Address model, Django gives us a way to 70 | access things in the reverse direction. We can also use the double 71 | underscore notation to filter Addresses or Contacts based on the 72 | related objects. 73 | 74 | Let's go ahead and add address display to our contacts. We'll add the 75 | list of all Addresses to the Contact detail view in ``contact.html``. 76 | 77 | .. literalinclude:: /src/contacts/templates/contact.html 78 | 79 | Editing Related Objects 80 | ======================= 81 | 82 | So how do we go about editing addresses for our contacts? You can 83 | imagine creating another CreateView like we did for Contacts, but the 84 | question remains: how do we wire the new Address to our Contact? We 85 | could conceivably just pass the Contact's ID through the the HTML, but 86 | we'd still need to validate that it hadn't been tampered with when we 87 | go to create the Address. 88 | 89 | To deal with this, we'll create a form that understands the 90 | relationship between Contacts and Addresses. 91 | 92 | .. checkpoint:: edit_addresses 93 | 94 | The editing interface we're going to build for Addresses is one that 95 | allows you to edit all the addresses for a Contact at once. To do 96 | this, we'll need to create a FormSet_ that handles all the Addresses 97 | for a single Contact. A FormSet is an object that manages multiple 98 | copies of the same Form (or ModelForm) in a single page. The `Inline 99 | FormSet`_ does this for a set of objects (in this case Addresses) that 100 | share a common related object (in this case the Contact). 101 | 102 | Because formsets are somewhat complex objects, Django provides factory 103 | functions that create the class for you. We'll add a call to the 104 | factory to our ``forms.py`` file. 105 | 106 | .. literalinclude:: /src/contacts/forms.py 107 | :lines: 3-8,40- 108 | 109 | When we create the view, we'll need to specify that this is the form 110 | we want to use, instead of having Django create one for us. 111 | 112 | .. literalinclude:: /src/contacts/views.py 113 | :pyobject: EditContactAddressView 114 | 115 | Note that even though we're editing Addresses with this view, we still 116 | have ``model`` set to ``Contact``. This is because an inline formset 117 | takes the parent object as its starting point. 118 | 119 | Once again, this needs to be wired up into the URL configuration. 120 | 121 | .. literalinclude:: /src/addressbook/urls.py 122 | :lines: 16-17 123 | 124 | And we have a simple template. 125 | 126 | .. literalinclude:: /src/contacts/templates/edit_addresses.html 127 | :language: html 128 | 129 | There are two new things in this template, both related to the fact 130 | we're using a formset instead of a form. First, there's a reference to 131 | ``form.management_form``. This is a set of hidden fields that provide 132 | some accounting information to Django: how many forms did we start 133 | with, how many empty ones are there, etc. If Django can't find this 134 | information when you POST the form, it will raise an exception. 135 | 136 | Second, we're iterating over form instead of just outputting it (``for 137 | address_form in form``). Again, this is because ``form`` here is a 138 | formset instead of a single form. When you iterate over a formset, 139 | you're iterating over the individual forms in it. These individual 140 | forms are just "normal" ``ModelForm`` instances for each Address, so 141 | you can apply the same output techniques you would normally use. 142 | 143 | .. _FormSet: https://docs.djangoproject.com/en/1.5/topics/forms/formsets/ 144 | .. _`Inline FormSet`: https://docs.djangoproject.com/en/1.5/topics/forms/modelforms/#inline-formsets 145 | -------------------------------------------------------------------------------- /tutorial/static.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | .. slideconf:: 5 | :autoslides: True 6 | :theme: single-level 7 | 8 | =================== 9 | Using Static Assets 10 | =================== 11 | 12 | .. checkpoint:: static_files 13 | 14 | Now that we have a basic application where we can add contacts and 15 | list them, it's reasonable to think about how we'd make this look more 16 | appealing. Most modern web applications are a combination of server 17 | side code/views, and client side, static assets, such as JavaScript 18 | and CSS. Regardless of whether you choose JavaScript or CoffeeScript, 19 | CSS or SASS, Django provides support for integrating static assets 20 | into your project. 21 | 22 | 23 | Adding Static Files 24 | =================== 25 | 26 | Django distinguishes between "static" and "media" files. The former 27 | are static assets included with your app or project. The latter are 28 | files uploaded by users using one of the file storage backends. Django 29 | includes a contrib app, ``django.contrib.staticfiles`` for managing 30 | static files and, importantly, generating the URLs to them. You could, 31 | of course, simply hard code the URLs to your static assets, and that'd 32 | probably work for a while. But if you want to move your static assets 33 | to their own server, or to a CDN, using generated URLs let's you make 34 | that change without needing to update your templates. 35 | ``django.contrib.staticfiles`` is enabled by default when you create a 36 | new project, so you can just start using it. 37 | 38 | We're going to add Bootstrap_ to our project for some basic styling. 39 | You can download the Bootstrap files from its website, 40 | http://getbootstrap.com. 41 | 42 | .. _Bootstrap: http://getbootstrap.com 43 | 44 | Django supports adding static files at both the application and 45 | project level. Where you add them sort of depends on how tied to your 46 | specific assembly of apps they are. That is, are they reusable for 47 | anyone using your app, or are they specific to your particular 48 | deployment? 49 | 50 | App specific static files are stored in the ``static`` subdirectory 51 | within the app. Django will also look in any directories listed in the 52 | ``STATICFILES_DIRS`` setting. Let's update our project settings to 53 | specify a static files directory. 54 | 55 | .. literalinclude:: /src/addressbook/settings.py 56 | :language: python 57 | :prepend: import os.path 58 | ... 59 | :lines: 67-77 60 | 61 | Note that we use ``os.path`` to construct the absolute path. This 62 | ensures Django can locate the files unambiguously. 63 | 64 | Let's go ahead and create the static directory in our project and 65 | unpack Bootstrap into it. 66 | 67 | :: 68 | 69 | (tutorial)$ mkdir addressbook/static 70 | (tutorial)$ cd addressbook/static 71 | (tutorial)$ unzip ~/Downloads/bootstrap.zip 72 | Archive: /Users/nathan/Downloads/bootstrap.zip 73 | creating: bootstrap/ 74 | creating: bootstrap/css/ 75 | inflating: bootstrap/css/bootstrap-responsive.css 76 | inflating: bootstrap/css/bootstrap-responsive.min.css 77 | inflating: bootstrap/css/bootstrap.css 78 | inflating: bootstrap/css/bootstrap.min.css 79 | creating: bootstrap/img/ 80 | inflating: bootstrap/img/glyphicons-halflings-white.png 81 | inflating: bootstrap/img/glyphicons-halflings.png 82 | creating: bootstrap/js/ 83 | inflating: bootstrap/js/bootstrap.js 84 | inflating: bootstrap/js/bootstrap.min.js 85 | 86 | 87 | Referring to Static Files in Templates 88 | ====================================== 89 | 90 | The Django staticfiles app includes a `template tag`_ that make it 91 | easy to refer to static files within your templates. You load template 92 | tag libraries using the ``load`` tag. 93 | 94 | .. _`template tag`: https://docs.djangoproject.com/en/1.5/ref/templates/builtins/ 95 | 96 | :: 97 | 98 | {% load staticfiles %} 99 | 100 | After loading the static files library, you can refer to the file 101 | using the ``static`` tag. 102 | 103 | :: 104 | 105 | 107 | 108 | Note that the path we specify is *relative* to the static files 109 | directory. Django is going to join this path with the ``STATIC_URL`` 110 | setting to generate the actual URL to use. 111 | 112 | The `STATIC_URL setting`_ tells Django what the root URL for your 113 | static files is. By default it's set to ``/static/``. 114 | 115 | .. _`STATIC_URL setting`: https://docs.djangoproject.com/en/1.5/ref/settings/#std:setting-STATIC_URL 116 | 117 | Simple Template Inclusion 118 | ========================= 119 | 120 | We want to add the Boostrap CSS to all of our templates, but we'd like 121 | to avoid repeating ourself: if we add it to each template 122 | individually, when we want to make changes (for example, to add 123 | another stylesheet) we have to make them to all the files. To solve 124 | this, we'll create a base template that the others will inherit from. 125 | 126 | Let's create ``base.html`` in the ``templates`` directory of our 127 | ``contacts`` app. 128 | 129 | .. literalinclude:: /src/contacts/templates/base.html 130 | 131 | ``base.html`` defines the common structure for our pages, and includes 132 | a ``block`` tag, which other templates can fill in. 133 | 134 | We'll update ``contact_list.html`` to extend from ``base.html`` and 135 | fill in the ``content`` block. 136 | 137 | .. literalinclude:: /src/contacts/templates/contact_list.html 138 | 139 | Serving Static Files 140 | ==================== 141 | 142 | We've told Django where we store our static files, and we've told it 143 | what URL structure to use, but we haven't actually connected the two 144 | together. Django doesn't serve static files by default, and for good 145 | reason: using an application server to serve static resources is going 146 | to be ineffecient, at best. The Django documentation on `deploying 147 | static files`_ does a good job of walking through the options for 148 | getting your static files onto your CDN or static file server. 149 | 150 | For development, however, it's convenient to do it all with one 151 | process, so there's a helper. We'll update our ``addressbook/urls.py`` 152 | file to include the ``staticfiles_urlpatterns`` helper. 153 | 154 | .. literalinclude:: /src/addressbook/urls.py 155 | 156 | Now we can run the server and see our newly Boostrapped templates in 157 | action. 158 | 159 | .. image:: 160 | /_static/tutorial/boostrapped.png 161 | 162 | Review 163 | ====== 164 | 165 | * Django distinguishes between static site files, and user uploaded 166 | media 167 | * The ``staticfiles`` app is included to help manage static files and 168 | serve them during development 169 | * Static files can be included with apps, or with the project. Choose 170 | where you put them based on whether you expect all users of your app 171 | to need them. 172 | * Templates can extend one another, using ``block`` tags. 173 | 174 | .. _`deploying static files`: https://docs.djangoproject.com/en/1.5/howto/static-files/deployment/ 175 | 176 | .. ifslides:: 177 | 178 | * Next: :doc:`additional-views` 179 | -------------------------------------------------------------------------------- /tutorial/views.rst: -------------------------------------------------------------------------------- 1 | .. tut:: 2 | :path: /src 3 | 4 | .. slideconf:: 5 | :autoslides: False 6 | :theme: single-level 7 | 8 | =============== 9 | Writing Views 10 | =============== 11 | 12 | .. slide:: Writing Views 13 | :level: 1 14 | 15 | Handling HTTP Requests from users. 16 | 17 | .. rst-class:: include-as-slide, slide-level-2 18 | 19 | View Basics 20 | =========== 21 | 22 | Django Views take an `HTTP Request`_ and return an `HTTP Response`_ to 23 | the user. 24 | 25 | .. blockdiag:: 26 | 27 | blockdiag { 28 | // Set labels to nodes. 29 | A [label = "User"]; 30 | C [label = "View"]; 31 | 32 | A -> C [label = "Request"]; 33 | C -> A [label = "Response"]; 34 | } 35 | 36 | Any Python callable can be a view. The only hard and fast requirement 37 | is that it takes the request object (customarily named ``request``) as 38 | its first argument. This means that a minimalist view is super 39 | simple:: 40 | 41 | 42 | 43 | from django.http import HttpResponse 44 | 45 | def hello_world(request): 46 | return HttpResponse("Hello, World") 47 | 48 | Of course, like most frameworks, Django also allows you to pass 49 | arguments to the view from the URL. We'll cover this as we build up 50 | our application. 51 | 52 | .. _`HTTP Request`: https://docs.djangoproject.com/en/1.5/ref/request-response/#httprequest-objects 53 | .. _`HTTP Response`: https://docs.djangoproject.com/en/1.5/ref/request-response/#httpresponse-objects 54 | 55 | .. rst-class:: include-as-slide, slide-level-2 56 | 57 | Generic & Class Based Views 58 | =========================== 59 | 60 | * `Generic Views`_ have always provided some basic functionality: 61 | render a template, redirect, create or edit a model, etc. 62 | * Django 1.3 introduced `Class Based Views`_ (CBV) for the generic views 63 | * Provide higher levels of abstraction and composability 64 | * Also hide a lot of complexity, which can be confusing for the 65 | newcomer 66 | * Luckily the documentation is **much** better with Django 1.5 67 | 68 | .. ifnotslides:: 69 | 70 | Django 1.3 introduced class based views, which is what we'll be 71 | focusing on here. Class based views, or CBVs, can eliminate a lot of 72 | boilerplate from your views, especially for things like an edit view 73 | where you want to take different action on a ``GET`` vs ``POST``. They 74 | give you a lot of power to compose functionality from pieces. The 75 | downside is that this power comes with some added complexity. 76 | 77 | 78 | .. rst-class:: include-as-slide, slide-level-2 79 | 80 | Class Based Views 81 | ================= 82 | 83 | The minimal class based view subclasses View_ and implements methods 84 | for the HTTP methods it supports. Here's the class-based version of 85 | the minimalist "Hello, World" view we previously wrote. 86 | 87 | :: 88 | 89 | from django.http import HttpResponse 90 | from django.views.generic import View 91 | 92 | class MyView(View): 93 | 94 | def get(self, request, *args, **kwargs): 95 | return HttpResponse("Hello, World") 96 | 97 | In a class based view, HTTP methods map to class method names. In this 98 | case, we've defined a handler for ``GET`` requests with the ``get`` 99 | method. Just like the function implementation, it takes ``request`` as 100 | its first argument, and returns an HTTP Response. 101 | 102 | .. sidebar:: Permissive Signatures 103 | 104 | You may notice that it has a couple of extra arguments in its 105 | signature, compared to the view we saw previously, specifically 106 | ``*args`` and ``**kwargs``. Class based views were first introduced 107 | as a way to make Django's "generic" views more flexible. That meant 108 | they were used in many different contexts, with potentially 109 | different arguments extracted from the URLs. As I've been writing 110 | class based views over the past year, I've continued to write them 111 | with permissive signatures, as I've found they're often useful in 112 | ways I didn't initially expect. 113 | 114 | Listing Contacts 115 | ================ 116 | 117 | .. slide:: List Views 118 | :level: 2 119 | 120 | ListView_ provides a view of a set of objects. 121 | 122 | :: 123 | 124 | class ContactsList(ListView): 125 | 126 | model = Contact 127 | template_name = 'contact_list.html' 128 | 129 | def get_queryset(self): 130 | ... # defaults to model.objects.all() 131 | 132 | def get_context_object_name(self): 133 | ... # defaults to _list 134 | 135 | def get_context_data(self, **kwargs): 136 | ... # add anything else to the context 137 | 138 | def get_context_data(self, **kwargs): 139 | ... # add anything else to the context 140 | 141 | .. slide:: Edit Views 142 | :level: 2 143 | 144 | CreateView_, UpdateView_, DeleteView_ work on a model instance. 145 | :: 146 | 147 | class UpdateContact(UpdateView): 148 | model = Contact 149 | template_name = 'edit_contact.html' 150 | 151 | def get_object(self): 152 | ... # defaults to looking for a pk or slug kwarg, and 153 | # passing that to filter 154 | 155 | def get_context_object_name(self): 156 | ... # defaults to 157 | def get_context_data(self, **kwargs): 158 | ... # add anything else to the context 159 | 160 | def get_success_url(self): 161 | ... # where to redirect to on success 162 | # defaults to self.get_object().get_absolute_url() 163 | 164 | .. slide:: Detail Views 165 | :level: 2 166 | 167 | DetailView_ provides a view of a single object 168 | 169 | :: 170 | 171 | class ContactView(DetailView): 172 | 173 | model = Contact 174 | template_name = 'contact.html' 175 | 176 | def get_object(self): 177 | ... # defaults to looking for a pk or slug kwarg, and 178 | # passing that to filter 179 | 180 | def get_context_object_name(self): 181 | ... # defaults to 182 | 183 | def get_context_data(self, **kwargs): 184 | ... # add anything else to the context 185 | 186 | .. checkpoint:: contact_list_view 187 | 188 | We'll start with a view that presents a list of contacts in the 189 | database. 190 | 191 | The basic view implementation is shockingly brief. We can write the 192 | view in just a few lines in the ``views.py`` file in our ``contacts`` 193 | application. 194 | 195 | .. literalinclude:: /src/contacts/views.py 196 | :language: python 197 | :end-before: template_name 198 | 199 | The ListView_ that we subclass from is itself composed of several 200 | mixins that provide some behavior, and that composition gives us a lot 201 | of power without a lot of code. In this case we set ``model = 202 | Contact``, which says that this view is going to list *all* the 203 | Contacts in our database. 204 | 205 | 206 | .. rst-class:: include-as-slide, slide-level-2 207 | 208 | Defining URLs 209 | ============= 210 | 211 | The URL configuration tells Django how to match a request's path to 212 | your Python code. Django looks for the URL configuration, defined as 213 | ``urlpatterns``, in the ``urls.py`` file in your project. 214 | 215 | Let's add a URL mapping for our contact list view in 216 | ``addressbook/urls.py``. 217 | 218 | .. literalinclude:: /src/addressbook/urls.py 219 | :language: python 220 | 221 | 222 | * Use of the ``url()`` function is not strictly required, but I like 223 | it: when you start adding more information to the URL pattern, it 224 | lets you use named parameters, making everything more clear. 225 | * The first parameter is a regular expression. Note the trailing 226 | ``$``; why might that be important? 227 | * The second parameter is the view callable. It can either be the 228 | actual callable (imported manually), or a string describing it. If 229 | it's a string, Django will import the module (up to the final dot), 230 | and then calls the final segment when a request matches. 231 | * Note that when we're using a class based view, we *must* use the 232 | real object here, and not the string notation. That's because we 233 | have to call the class method ``as_view()``, which returns a wrapper 234 | around our class that Django's URL dispatch can call. 235 | * Giving a URL pattern a name allows you to do a reverse lookup 236 | * The URL name is useful when linking from one View to another, or 237 | redirecting, as it allows you to manage your URL structure in one 238 | place 239 | 240 | While the ``urlpatterns`` name **must** be defined, Django also allows 241 | you to define a few other values in the URL configuration for 242 | exceptional cases. These include ``handler403``, ``handler404``, and 243 | ``handler500``, which tell Django what view to use when an HTTP error 244 | occurs. See the `Django urlconf documentation`_ for details. 245 | 246 | .. _`Django urlconf documentation`: https://docs.djangoproject.com/en/1.5/ref/urls/#handler403 247 | 248 | .. admonition:: URL Configuration Import Errors 249 | 250 | Django loads the URL configuration very early during startup, and 251 | will attempt to import things it finds here. If one of the imports 252 | fails, however, the error message can be somewhat opaque. If your 253 | project stops working with an import-related exception, try to 254 | import the URL configuration in the interactive shell. That usually 255 | makes it clear where the problem lies. 256 | 257 | 258 | Creating the Template 259 | ===================== 260 | 261 | .. slide:: Django Templates 262 | :level: 2 263 | 264 | * Django allows you to specify ``TEMPLATE_DIRS`` to look for templates 265 | in 266 | * By default it looks for a ``template`` subdirectory in each app 267 | * Keeping templates within an app makes creating reusable apps easier 268 | 269 | Now that we've defined a URL for our list view, we can try it out. 270 | Django includes a server suitable for development purposes that you 271 | can use to easily test your project:: 272 | 273 | $ python manage.py runserver 274 | Validating models... 275 | 276 | 0 errors found 277 | Django version 1.4.3, using settings 'addressbook.settings' 278 | Development server is running at http://127.0.0.1:8000/ 279 | Quit the server with CONTROL-C. 280 | 281 | If you visit the ``http://localhost:8000/`` in your browser, though, 282 | you'll see an error: ``TemplateDoesNotExist``. 283 | 284 | .. image:: 285 | /_static/tutorial/TemplateDoesNotExist.png 286 | 287 | Most of Django's generic views (such as ``ListView`` which we're 288 | using) have a predefined template name that they expect to find. We 289 | can see in this error message that this view was expecting to find 290 | ``contact_list.html``, which is derived from the model name. Let's go 291 | and create that. 292 | 293 | By default Django will look for templates in applications, as well as 294 | in directories you specify in ``settings.TEMPLATE_DIRS``. The generic 295 | views expect that the templates will be found in a directory named 296 | after the application (in this case ``contacts``), and the filename 297 | will contain the model name (in this case ``contact_list.html``). This 298 | works very well when you're distributing a reusable application: the 299 | consumer can create templates that override the defaults, and they're 300 | clearly stored in a directory associated with the application. 301 | 302 | For our purposes, however, we don't need that extra layer of directory 303 | structure, so we'll specify the template to use explicitly, using the 304 | ``template_name`` property. Let's add that one line to ``views.py``. 305 | 306 | .. literalinclude:: /src/contacts/views.py 307 | 308 | Create a ``templates`` subdirectory in our ``contacts`` application, 309 | and create ``contact_list.html`` there. 310 | 311 | .. literalinclude:: /src/contacts/templates/contact_list.html 312 | :language: html 313 | 314 | Opening the page in the browser, we should see one contact there, the 315 | one we added earlier through the interactive shell. 316 | 317 | Creating Contacts 318 | ================= 319 | 320 | .. checkpoint:: create_contact_view 321 | 322 | Adding information to the database through the interactive shell is 323 | going to get old fast, so let's create a view for adding a new 324 | contact. 325 | 326 | Just like the list view, we'll use one of Django's generic views. In 327 | ``views.py``, we can add the new view: 328 | 329 | .. literalinclude:: /src/contacts/views.py 330 | :prepend: from django.core.urlresolvers import reverse 331 | from django.views.generic import CreateView 332 | ... 333 | :pyobject: CreateContactView 334 | 335 | Most generic views that do form processing have the concept of the 336 | "success URL": where to redirect the user when the form is 337 | successfully submitted. The form processing views all adhere to the 338 | practice of POST-redirect-GET for submitting changes, so that 339 | refreshing the final page won't result in form re-submission. You can 340 | either define this as a class property, or override the 341 | ``get_success_url()`` method, as we're doing here. In this case we're 342 | using the ``reverse`` function to calculate the URL of the contact 343 | list. 344 | 345 | .. sidebar:: Context Variables in Class Based Views 346 | 347 | The collection of values available to a template when it's rendered 348 | is referred to as the Context. The Context is a combination of 349 | information supplied by the view and information from `context 350 | processors`_. 351 | 352 | When you're using built in generic views, it's not obvious what 353 | values are available to the context. With some practice you'll 354 | discover they're pretty consistent -- ``form``, ``object``, and 355 | ``object_list`` are often used -- but that doesn't help when you're 356 | just starting off. Luckily, the documentation for this is much 357 | improved with Django 1.5. 358 | 359 | In class based views, the ``get_context_data()`` method is used to 360 | add information to the context. If you override this method, you 361 | usually want to accept ``**kwargs``, and call the super class. 362 | 363 | The template is slightly more involved than the list template, but not 364 | much. Our ``edit_contact.html`` will look something like this. 365 | 366 | .. literalinclude:: /src/contacts/templates/edit_contact.html 367 | :language: html 368 | 369 | A few things to note: 370 | 371 | - The ``form`` in the context is the `Django Form`_ for our model. 372 | Since we didn't specify one, Django made one for us. How thoughtful. 373 | - If we just write ``{{ form }}`` we'll get table rows; adding 374 | ``.as_ul`` formats the inputs for an unordered list. Try ``.as_p`` 375 | instead to see what you get. 376 | - When we output the form, it only includes our fields, not the 377 | surrounding ``
    `` tag or the submit button, so we have to add 378 | those. 379 | - The ``{% csrf_token %}`` tag inserts a hidden input that Django uses 380 | to verify that the request came from your project, and isn't a 381 | forged cross-site request. Try omitting it: you can still access the 382 | page, but when you go to submit the form, you'll get an error. 383 | - We're using the ``url`` template tag to generate the link back to 384 | the contact list. Note that ``contacts-list`` is the name of our 385 | view from the URL configuration. By using ``url`` instead of an 386 | explicit path, we don't have to worry about a link breaking. ``url`` 387 | in templates is equivalent to ``reverse`` in Python code. 388 | 389 | You can configure the URL by adding the following line to our 390 | ``urls.py`` file:: 391 | 392 | url(r'^new$', contacts.views.CreateContactView.as_view(), 393 | name='contacts-new',), 394 | 395 | Now you can go to ``http://localhost:8000/new`` to create new contacts. 396 | 397 | To complete the story, let's add a link to `contact_list.html`. 398 | 399 | .. literalinclude:: /src/contacts/templates/contact_list.html 400 | :language: html 401 | 402 | 403 | Testing Your Views 404 | ================== 405 | 406 | .. slide:: Test Client & RequestFactory 407 | :level: 2 408 | 409 | * Views transform a Request into a Response, but still have logic 410 | * Test ``Client`` and ``RequestFactory`` are tools to help test them 411 | * They share a common API, but work slightly differently 412 | * Test ``Client`` resolves a URL to the view, returns a Response 413 | * ``RequestFactory`` generates a Request which you can pass to the View 414 | directly 415 | 416 | .. slide:: Test Client vs. RequestFactory 417 | :level: 2 418 | 419 | :: 420 | 421 | from django.test.client import Client 422 | from django.test.client import RequestFactory 423 | 424 | client = Client() 425 | response = client.get('/') 426 | 427 | :: 428 | 429 | factory = RequestFactory() 430 | request = factory.get('/') 431 | 432 | response = ListContactView.as_view()(request) 433 | 434 | So far our views have been pretty minimal: they leverage Django's 435 | generic views, and contain very little of our own code or logic. One 436 | perspective is that this is how it should be: a view takes a request, 437 | and returns a response, delegating the issue of validating input to 438 | forms, and business logic to model methods. This is a perspective that 439 | I subscribe to. The less logic contained in views, the better. 440 | 441 | However, there is code in views that should be tested, either by unit 442 | tests or integration tests. The distinction is important: unit tests 443 | are focused on testing a single unit of functionality. When you're 444 | writing a unit test, the assumption is that everything else has its 445 | own tests and is working properly. Integration tests attempt to test 446 | the system from end to end, so you can ensure that the points of 447 | integration are functioning properly. Most systems have both. 448 | 449 | Django has two tools that are helpful for writing unit tests for 450 | views: the Test Client_ and the RequestFactory_. They have similar 451 | APIs, but approach things differently. The ``TestClient`` takes a URL 452 | to retrieve, and resolves it against your project's URL configuration. 453 | It then creates a test request, and passes that request through your 454 | view, returning the Response. The fact that it requires you to specify 455 | the URL ties your test to the URL configuration of your project. 456 | 457 | The ``RequestFactory`` has the same API: you specify the URL you want 458 | to retrieve and any parameters or form data. But it doesn't actually 459 | resolve that URL: it just returns the Request object. You can then 460 | manually pass it to your view and test the result. 461 | 462 | In practice, RequestFactory tests are usually somewhat faster than the 463 | TestClient. This isn't a big deal when you have five tests, but it is 464 | when you have 500 or 5,000. Let's look at the same test written with 465 | each tool. 466 | 467 | .. checkpoint:: view_tests 468 | 469 | .. literalinclude:: /src/contacts/tests.py 470 | :prepend: from django.test.client import Client 471 | from django.test.client import RequestFactory 472 | ... 473 | from contacts.views import ListContactView 474 | ... 475 | :pyobject: ContactListViewTests 476 | 477 | 478 | Integration Tests 479 | ================= 480 | 481 | .. slide:: Live Server Tests 482 | :level: 2 483 | 484 | * Django 1.4 added the LiveServerTestCase_ 485 | * Makes writing integration tests with something like Selenium_ 486 | easier 487 | * By default it spins up the server for you (similar to ``runserver``) 488 | * You can also point it to a deployed instance elsewhere 489 | 490 | Django 1.4 adds a new ``TestCase`` base class, the 491 | LiveServerTestCase_. This is very much what it sounds like: a test 492 | case that runs against a live server. By default Django will start the 493 | development server for you when it runs these tests, but they can also 494 | be run against another server. 495 | 496 | Selenium_ is a tool for writing tests that drive a web browser, and 497 | that's what we'll use for our integration tests. By using Selenium, 498 | you're able to automate different browers (Chrome, Firefox, etc), and 499 | interact with your full application much as the user would. Before 500 | writing tests to use it, we'll need to install the Python implementation. 501 | 502 | :: 503 | 504 | (tutorial)$ pip install selenium 505 | 506 | We're going to write a couple of tests for our views: 507 | 508 | - one that creates a Contact and makes sure it's listed 509 | - one that makes sure our "add contact" link is visible and linked on 510 | the list page 511 | - and one that actually exercises the add contact form, filling it in 512 | and submitting it. 513 | 514 | .. literalinclude:: /src/contacts/tests.py 515 | :prepend: from django.test import LiveServerTestCase 516 | from selenium.webdriver.firefox.webdriver import WebDriver 517 | ... 518 | :pyobject: ContactListIntegrationTests 519 | 520 | Note that Selenium allows us to find elements in the page, inspect 521 | their state, click them, and send keystrokes. In short, it's like 522 | we're controlling the browser. In fact, if you run the tests now, 523 | you'll see a browser open when the tests run. 524 | 525 | In our example we're using CSS Selectors to locate elements in the 526 | DOM, but you can also use Xpath. For many people it's a matter of 527 | preference, but I've found that using CSS Selectors is often less 528 | brittle: if I change the markup, I'm likely to leave classes on 529 | important elements in place, even if their relative position in the 530 | DOM changes. 531 | 532 | Review 533 | ====== 534 | 535 | * Views take an HttpRequest_ and turn it into an HttpResponse_ 536 | * Generic class-based views introduced with Django 1.3 537 | * These let you create reusable, composable views 538 | * URLs are defined in ``urls.py`` in your project 539 | * Naming URLs lets you calculate the URL to a view 540 | * RequestFactory_ creates Requests for testing Views 541 | with 542 | * LiveServerTestCase_ provides basis for writing integration tests 543 | 544 | .. ifslides:: 545 | 546 | * Next: :doc:`static` 547 | 548 | 549 | .. _`Generic Views`: https://docs.djangoproject.com/en/1.5/topics/class-based-views/generic-display/ 550 | .. _`Class Based Views`: https://docs.djangoproject.com/en/1.5/topics/class-based-views/ 551 | .. _View: https://docs.djangoproject.com/en/1.5/ref/class-based-views/base/#view 552 | .. _ListView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-display/#listview 553 | .. _UpdateView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-editing/#updateview 554 | .. _CreateView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-editing/#createview 555 | .. _DeleteView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-editing/#deleteview 556 | .. _DetailView: https://docs.djangoproject.com/en/1.5/ref/class-based-views/generic-display/#detailview 557 | .. _`context processors`: https://docs.djangoproject.com/en/1.5/ref/templates/api/#subclassing-context-requestcontext 558 | .. _`Django Form`: https://docs.djangoproject.com/en/1.5/topics/forms/ 559 | .. _HttpRequest: https://docs.djangoproject.com/en/1.5/ref/request-response/#httprequest-objects 560 | .. _HttpResponse: https://docs.djangoproject.com/en/1.5/ref/request-response/#httpresponse-objects 561 | .. _Client: https://docs.djangoproject.com/en/1.5/topics/testing/overview/#module-django.test.client 562 | .. _RequestFactory: https://docs.djangoproject.com/en/1.5/topics/testing/advanced/#django.test.client.RequestFactory 563 | .. _LiveServerTestCase: https://docs.djangoproject.com/en/1.5/topics/testing/overview/#liveservertestcase 564 | .. _Selenium: http://seleniumhq.org/ 565 | --------------------------------------------------------------------------------