├── .gitignore └── doc ├── Makefile ├── admin_login.png ├── cannot-import-name-recipecreateview.png ├── conf.py ├── csrf_verification_failed.png ├── food_create_blue_button.png ├── food_create_boring.png ├── food_create_crispy.png ├── food_create_view_missing.png ├── food_detail.png ├── food_detail_template_missing.png ├── food_list.png ├── food_objects.png ├── food_template_does_not_exist.png ├── food_with_this_name_already_exists.png ├── improperly_configured.png ├── improperly_configured_recipes_create.png ├── index.rst ├── it_worked.png ├── no-reverse-match-recipe-create.png ├── no_admin_tables.png ├── no_module_named_urls.png ├── no_url_to_redirect_to.png ├── page_not_found.png ├── recipe-create-view-not-defined.png ├── recipe-list.png ├── two_of_the_same.png └── user_friendly_food_objects.png /.gitignore: -------------------------------------------------------------------------------- 1 | /env 2 | *.db 3 | _build 4 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Djecipes.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Djecipes.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Djecipes" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Djecipes" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/admin_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/admin_login.png -------------------------------------------------------------------------------- /doc/cannot-import-name-recipecreateview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/cannot-import-name-recipecreateview.png -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Recipes documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Oct 24 13:44:06 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 sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Django by errors' 44 | copyright = u'2012, Sigurd Gartmann' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.2' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.2' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'django-by-errors' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'django-by-errors.tex', u'Django by errors', 187 | u'Sigurd Gartmann', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'django-by-errors', u'Django by errors', 217 | [u'Sigurd Gartmann'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'django-by-errors', u'Django by errors', 231 | u'Sigurd Gartmann', 'Django by errors', 'A different Django tutorial', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /doc/csrf_verification_failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/csrf_verification_failed.png -------------------------------------------------------------------------------- /doc/food_create_blue_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_create_blue_button.png -------------------------------------------------------------------------------- /doc/food_create_boring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_create_boring.png -------------------------------------------------------------------------------- /doc/food_create_crispy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_create_crispy.png -------------------------------------------------------------------------------- /doc/food_create_view_missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_create_view_missing.png -------------------------------------------------------------------------------- /doc/food_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_detail.png -------------------------------------------------------------------------------- /doc/food_detail_template_missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_detail_template_missing.png -------------------------------------------------------------------------------- /doc/food_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_list.png -------------------------------------------------------------------------------- /doc/food_objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_objects.png -------------------------------------------------------------------------------- /doc/food_template_does_not_exist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_template_does_not_exist.png -------------------------------------------------------------------------------- /doc/food_with_this_name_already_exists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/food_with_this_name_already_exists.png -------------------------------------------------------------------------------- /doc/improperly_configured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/improperly_configured.png -------------------------------------------------------------------------------- /doc/improperly_configured_recipes_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/improperly_configured_recipes_create.png -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Djecipes documentation master file, created by 2 | sphinx-quickstart on Wed Oct 24 13:44:06 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ############################################## 7 | Django by errors - a different Django tutorial 8 | ############################################## 9 | 10 | ************ 11 | Introduction 12 | ************ 13 | 14 | In this tutorial, we will focus on how to build a recipe site by taking one 15 | step at a time, seeing what error is waiting for us next. 16 | 17 | When learning a new programming language or framework, there are so many guides 18 | and tutorials on how to do everything right. But what do you do the first time 19 | you get an error? 20 | 21 | Here, we will be accustomed to as many error messages as possible while 22 | building a small app to store recipes. 23 | 24 | In this tutorial, you will also learn how to use *South* for database 25 | migrations, and *Twitter Bootstrap* for a nice default site styling. 26 | 27 | About Django 28 | ============ 29 | 30 | `Django`_ is a web application framework written in `Python`_. As Python, 31 | it is Free software, licenced under the `BSD license`_, and it has lots of 32 | documentation and a large community. 33 | 34 | .. _Django: https://www.djangoproject.com/ 35 | .. _Python: http://www.python.org/ 36 | .. _BSD license: http://en.wikipedia.org/wiki/BSD_licenses 37 | 38 | Django fosters **rapid development**, using an easy-to-learn and easy-to-read 39 | language like Python will let you not only make the computer understand what 40 | you mean, but also the people who may eventually inherit your scripts. 41 | 42 | The components of Django are **loosely coupled**, so you can use another templating 43 | language instead of Django's own, and you can even change the model layer to 44 | use Sqlalchemy instead of the built-in Django ORM (Object relational mapper: 45 | the layer that translates your models to database language). 46 | 47 | In this tutorial, you will create a project, and then an application in that 48 | project. Django is built in a way that you will also be able to use third party 49 | applications inside your project, and you will in most cases start with a 50 | couple of Django applications. You can then build your apps in your projects in 51 | a way that you can mix them around and **reuse** them in other projects. 52 | 53 | One of the most important parts is that you should not write the same code over 54 | and over again. So in Django projects you should not see long sections 55 | duplicated over and over. The "class based views" we are going to use is one 56 | good improvement to this, and the template inheritance system is another way 57 | you will **not repeat yourself**. 58 | 59 | ************ 60 | Simple steps 61 | ************ 62 | 63 | In this part, you will learn the most basic parts of Django, to get a feeling 64 | of how Django applications are built. 65 | 66 | We assume you have a computer with Python installed, as all modern operating 67 | systems come with Python installed. To check if python is installed, open up a 68 | terminal, write "python" and press enter. If the command was not found, you 69 | should install Python. 70 | 71 | We will also use "virtualenv". It should not be too hard to install, just check 72 | out their official `installation documentation`_. 73 | 74 | .. _installation documentation: http://www.virtualenv.org/en/latest/#installation 75 | 76 | Starting a project 77 | ================== 78 | 79 | Find a place where you want your project, and create a virtual environment to 80 | keep your requirements and dependencies separated from the rest of your python 81 | system:: 82 | 83 | $ virtualenv --no-site-packages env 84 | 85 | And activate your new virtual environment:: 86 | 87 | $ source env/bin/activate 88 | 89 | You are now "inside" this virtual environment. You can type "deactivate" to get 90 | out of it. Each time you open a terminal and want to work on a project in a 91 | virtual environment, you need to activate it using the command above. But be 92 | sure to be inside when installing Django:: 93 | 94 | $ pip install django 95 | 96 | Now that Django is installed you have a new command inside the "bin" folder in env, 97 | which you can use to start a new Django project. I have named mine 98 | "recipes-project":: 99 | 100 | $ ./env/bin/django-admin.py startproject recipes_project 101 | 102 | Go into the **project folder**, make the ``manage.py`` script runnable and 103 | start the *built-in webserver*:: 104 | 105 | $ cd recipes_project 106 | $ python manage.py runserver 107 | 108 | Now go to ``localhost:8000`` in your web browser. You can stop the server by 109 | pressing ``ctrl-c`` as suggested by the command output. 110 | 111 | .. image:: it_worked.png 112 | 113 | The page congratulates you, and tells you about your next steps: you should 114 | update your database settings and create an **app**. The apps are meant to be 115 | *reusable* components that you can tie together when building projects. We will 116 | do this after a little break. 117 | 118 | File structure before creating an app 119 | ------------------------------------- 120 | 121 | Inside the folder that was created with the ``start-project`` command, you will 122 | see the ``manage.py`` file and a folder with the same as the project folder 123 | itself. 124 | 125 | Contents of project folder:: 126 | 127 | $ ls 128 | manage.py recipes_project 129 | 130 | This subfolder is where project settings and common configuration is stored. 131 | 132 | If you list the contents of the subfolder, you will see a special ``__init__`` 133 | file that by being there makes the folder to be a Python *package*, a 134 | ``settings`` file, the toplevel ``url`` routing file, and a ``wsgi`` file for 135 | running the code by a server. You will need to notice the location of your 136 | settings.py and the urls.py for later:: 137 | 138 | $ ls recipes_project 139 | __init__.py settings.py urls.py wsgi.py 140 | __init__.pyc settings.pyc urls.pyc wsgi.pyc 141 | 142 | The pyc-files are compiled versions of the source files. We do not need to 143 | bother too much about them, and if you remove them, they will get recreated 144 | when the source files are run again. 145 | 146 | Database setup 147 | -------------- 148 | 149 | The welcoming page told us to setup the database. The database settings are 150 | part of the *settings.py* file in the configuration folder. Open up 151 | ``recipes_project/settings.py`` in your favourite text editor, and change the 152 | database settings: Append ``sqlite3`` to the ``ENGINE`` field and add a 153 | database name to the NAME field, "database.db" is a good name:: 154 | 155 | DATABASES = { 156 | 'default': { 157 | 'ENGINE': 'django.db.backends.sqlite3', 158 | 'NAME': 'database.db', 159 | 'USER': '', 160 | 'PASSWORD': '', 161 | 'HOST': '', 162 | 'PORT': '', 163 | } 164 | } 165 | 166 | The database name will be the name of a local file in your project folder. 167 | Sqlite is a single-file database system that is easy to use when developing, 168 | but not recommended in a production for large sites. 169 | 170 | Creating an app 171 | =============== 172 | 173 | The welcoming page also wanted you to create an app. Do this using the 174 | ``manage.py`` command in the project folder:: 175 | 176 | python manage.py startapp recipes 177 | 178 | This will create a new folder structure for the new app besides "manage.py" and the inner "recipes_project", like this:: 179 | 180 | $ ls 181 | manage.py recipes recipes_project 182 | 183 | And the new *recipes* folder contains this:: 184 | 185 | $ ls recipes 186 | __init__.py models.py tests.py views.py 187 | 188 | Activating the app 189 | ------------------ 190 | 191 | Now, you should enable your new app in the ``settings.py``, by adding the name 192 | of your app to the ``INSTALLED_APPS``, near the bottom of the file. The section 193 | should look something like:: 194 | 195 | INSTALLED_APPS = ( 196 | 'django.contrib.auth', 197 | 'django.contrib.contenttypes', 198 | 'django.contrib.sessions', 199 | 'django.contrib.sites', 200 | 'django.contrib.messages', 201 | 'django.contrib.staticfiles', 202 | # Uncomment the next line to enable the admin: 203 | # 'django.contrib.admin', 204 | # Uncomment the next line to enable admin documentation: 205 | # 'django.contrib.admindocs', 206 | 'recipes', 207 | ) 208 | 209 | The extra comma at the end is optional on the last line, but I recommend it, as 210 | it makes it easier to add another line later. 211 | 212 | Now, to route traffic to the newly created app, we also need to add a line to 213 | the list of url patterns Django will use to match incoming requests. In the 214 | project level ``urls.py``, you will see a line like this:: 215 | 216 | # url(r'^recipes_project/', include('recipes_project.foo.urls')), 217 | 218 | The code with "#" in front is "commented out" and will not run. To make it 219 | active, remove the "#". We will also change the line itself 220 | so it reads:: 221 | 222 | url(r'^recipes/', include('recipes.urls')), 223 | 224 | Every address that starts with "recipes" will now point to a urls file of 225 | your recipes app, where the actual routes to views will happen. 226 | 227 | It is useful to keep a terminal always running ``python manage.py runserver``, 228 | and use another terminal window or tab for all the other commands you need to 229 | run. Remember to run the ``activate`` command in your new terminal. 230 | 231 | Refresh the browser and see that complains: "No module named urls" 232 | 233 | .. image:: no_module_named_urls.png 234 | 235 | The line we just activated tells Django to look for url patterns in a file at 236 | "recipes/urls.py", but that file does not exist yet. Copy the urls.py from the 237 | project folder into the app folder, and remove all the commented code and url 238 | patterns so that the new file looks like this:: 239 | 240 | from django.conf.urls import patterns, include, url 241 | 242 | urlpatterns = patterns('', 243 | ) 244 | 245 | Go to the browser and refresh. Now it says "Page not found (404)" which is a 246 | generic error message about a page not being found, but this also tells you 247 | what alternatives you have. 248 | 249 | .. image:: page_not_found.png 250 | 251 | The page suggests that you should add ``/recipes/`` to the address field of 252 | your browser. In the urls.py you have created a rule that sends everything 253 | starting with ``recipes/`` to the recipes app. Go ahead, add recipes to the 254 | browser location, and see that you get the first "It worked!" page again as 255 | there were no errors, but also, no contents. 256 | 257 | 258 | *************************** 259 | Models, views and templates 260 | *************************** 261 | 262 | There are different ways to organize code so it will not end up as a pile of 263 | spaghetti. Have a look again in the recipes *app* folder, you'll see four 264 | files ending in '.py'. The *__init__* is needed for the Python module that the 265 | app is to work, *models* will contain your models, *tests* will contain your 266 | tests, and *views* is the code that will build up different "pages":: 267 | 268 | $ ls recipes 269 | __init__.py models.py tests.py urls.pyc 270 | __init__.pyc models.pyc urls.py views.py 271 | 272 | Later we will add *templates* as well: `HTML`_ code that will decide the layout 273 | and design of your pages. The templates folder is not created automatically as 274 | it is possible to put templates other places as well. 275 | 276 | .. _HTML: https://en.wikipedia.org/wiki/Html 277 | 278 | If you are coming from another language or framework, you will eventually see 279 | that the templates are stricter than you are used to. You are not allowed to 280 | put tons of functionality into the template code A graphical designer should 281 | be able to understand and change the templates without knowing Python or 282 | Django. 283 | 284 | Your first model: Food 285 | ====================== 286 | 287 | That's enough theory for a while. Now we will add a very simple model to 288 | ``models.py``. This is the model for all the types of food we will use in the 289 | recipes. It will only have one field we need to know of, the *name* of the food 290 | objects. Django will automatically give it an *id* field for the primary key. 291 | Add the following class to recipes/models.py:: 292 | 293 | class Food(models.Model): 294 | name = models.CharField(max_length=20) 295 | 296 | This model has to be used by the database. Django has a manage command called 297 | ``syncdb`` that will setup and all tables needed by Django for us. But wait a 298 | minute. Using a third party tool called *south* we can get database migrations 299 | as well. 300 | 301 | Set up database migration support 302 | --------------------------------- 303 | 304 | Database migrations let you script the database changes so you can go from one 305 | version to another without manually executing ``alter table`` or other `SQL`_ 306 | commands. You can also use this for data migrations, but we will not get into 307 | that now. You need a third party app called "South" to do this. There have been 308 | discussions about taking all or parts of South into the core of Django 309 | 310 | .. _SQL: https://en.wikipedia.org/wiki/Sql 311 | 312 | In settings.py, add ``'south',`` to the bottom of the INSTALLED_APPS to use 313 | that app as well as your own. When saving the file, the running "runserver" 314 | process will stop, telling:: 315 | 316 | Error: No module named south 317 | 318 | You need to install South:: 319 | 320 | $ pip install south 321 | 322 | And restart your server. 323 | 324 | To create your first migration belonging to the *recipes* app/module, use the 325 | *init* subcommand:: 326 | 327 | $ python manage.py schemamigration recipes --init 328 | 329 | This will only create the migration, not do anything to the database, as you 330 | can create more migrations and execute them at the same time. It will also 331 | prevent the *syncdb* command from creating your databases without migration 332 | support. 333 | 334 | To actually run this command, you need to run the management command 335 | ``migrate``. This will only take care of your new app (since this is the only 336 | one with migrations defined). To do both *syncdb* and *migrate* at the same 337 | time, run:: 338 | 339 | $ python manage.py syncdb --migrate 340 | 341 | The first time syncdb is run, it will ask you to create a user. We will soon be 342 | using the built-in admin interface where you later can create users, but to log 343 | in and create users, you need a user, so please answer "yes" and fill in the 344 | information. The output will look similar to this:: 345 | 346 | Superuser created successfully. 347 | Installing custom SQL ... 348 | Installing indexes ... 349 | Installed 0 object(s) from 0 fixture(s) 350 | Migrating... 351 | Running migrations for recipes: 352 | - Migrating forwards to 0001_initial. 353 | > recipes:0001_initial 354 | - Loading initial data for recipes. 355 | Installed 0 object(s) from 0 fixture(s) 356 | 357 | Synced: 358 | > django.contrib.auth 359 | > django.contrib.contenttypes 360 | > django.contrib.sessions 361 | > django.contrib.sites 362 | > django.contrib.messages 363 | > django.contrib.staticfiles 364 | > south 365 | 366 | Migrated: 367 | - recipes 368 | 369 | The output from the syncdb command states that all apps specified in 370 | INSTALLED_APPS, except for your recipes, has been set up using the normal 371 | syncdb, and that your recipes app has been set up using a migration. Good. 372 | 373 | Set up admin interface 374 | ---------------------- 375 | 376 | Now we will utilize the built-in Django Admin. In ``urls.py`` in the project 377 | folder, **uncomment** the lines regarding *admin*:: 378 | 379 | from django.conf.urls import patterns, include, url 380 | 381 | # Uncomment the next two lines to enable the admin: 382 | from django.contrib import admin 383 | admin.autodiscover() 384 | 385 | urlpatterns = patterns('', 386 | # Examples: 387 | # url(r'^$', 'recipes_project.views.home', name='home'), 388 | url(r'^recipes/', include('recipes.urls')), 389 | 390 | # Uncomment the admin/doc line below to enable admin documentation: 391 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 392 | 393 | # Uncomment the next line to enable the admin: 394 | url(r'^admin/', include(admin.site.urls)), 395 | ) 396 | 397 | We have already set up an url pattern to forward everything starting with 398 | *recipes/* to the python module *recipes.urls*, and now everything starting 399 | with "admin" will redirect to the admin interface we will soon take a closer 400 | look at. 401 | 402 | If you refresh your browser at this time, you will get an error about your site 403 | being improperly configured. 404 | 405 | .. image:: improperly_configured.png 406 | 407 | The error message suggests that you should put ``django.contrib.admin`` in the 408 | INSTALLED_APPS section of settings.py. It is already there, you just need to 409 | uncomment it. 410 | 411 | After uncommenting the admin app, have a look in your browser. No matter what 412 | address you go to, the server will not find it, and suggests you should try 413 | ``localhost:8000/admin/``. Go there and have a look. 414 | 415 | .. image:: admin_login.png 416 | 417 | You should now be able to log in and have a look around. You should see some 418 | predefined classes from Django like User and Group, but Admin can also take 419 | care of your Food model. To get that to work, you need to create a file in the 420 | recipes folder called "admin.py". The file should contain:: 421 | 422 | from django.contrib import admin 423 | from recipes.models import Food 424 | 425 | admin.site.register(Food) 426 | 427 | On browser refresh, nothing changes. When adding new models to admin, you need 428 | to restart the server. Just stop it (ctrl-c) and restart the runserver command. 429 | 430 | You should now be able to see your Food model in the list. Click on it and try 431 | to add some food objects, like "Banana" or "Apple". 432 | 433 | .. image:: no_admin_tables.png 434 | 435 | You will now get an error complaining about missing tables. This is because you 436 | added the admin inteface after the last run of "syncdb", so the tables admin 437 | needs are not created. Just run the same syncdb command again:: 438 | 439 | $ python manage.py syncdb --migrate 440 | 441 | This time, the output also lists "django.contrib.admin" as a synced app. 442 | 443 | Adding a method to your model 444 | ----------------------------- 445 | 446 | When you have successfully created Apple and Banana, you will see that you have two lines of *Food object*. 447 | 448 | .. image:: food_objects.png 449 | 450 | This is not very useful, as it is not possible to distinguish between the lines 451 | in the list. In your models.py add a function named ``__unicode__`` inside your 452 | Food class (at the same indentation level as the "name"). Make it return 453 | ``self.name``, like this:: 454 | 455 | def __unicode__(self): 456 | return self.name 457 | 458 | When refreshing the list, your table should look more user friendly. The 459 | __unicode__ is utilized by Django to write a human readable version of the 460 | object. A side effect is that you now have defined a default representation of 461 | the object, so you do not need to add ``.name`` everywhere. 462 | 463 | .. image:: user_friendly_food_objects.png 464 | 465 | Your first view: Food list 466 | ========================== 467 | 468 | Admin does everything nice and tidy, but you don't want to expose the admin 469 | inteface to your users. We have to create a simpler representation to show to 470 | our users. 471 | 472 | Open up ``recipes/views.py`` and paste in this code:: 473 | 474 | from django.shortcuts import render_to_response 475 | from django.template import RequestContext 476 | from recipes.models import Food 477 | 478 | def food_list(request): 479 | food = Food.objects.all() 480 | return render_to_response('recipes/food_list.html', {'object_list': food}, context_instance=RequestContext(request)) 481 | 482 | The ``food_list`` method will fetch all ``Food`` objects from the database. 483 | Hold them in a variable named ``food``, and send this variable to a *template* 484 | named ``food_list.html``, but as a variable named ``object_list`` exposed to 485 | the template. 486 | 487 | Go to your app's urls.py and add an import statement to the top:: 488 | 489 | from recipes.views import food_list 490 | 491 | And a line to the pattern list to get all food:: 492 | 493 | url(r'^food/$', food_list, name='food-list'), 494 | 495 | Now ``/recipes/food/`` should trigger the newly created ``food_list`` function. 496 | Go to this address and see what you get. 497 | 498 | .. image:: food_template_does_not_exist.png 499 | 500 | You got an error message. It tells you to make a template named 501 | "recipes/food_list.html". 502 | 503 | Bootstrapping a template 504 | ------------------------ 505 | 506 | We will make this template in a folder named "templates/recipes" inside the app 507 | folder:: 508 | 509 | $ mkdir -p templates/recipes 510 | 511 | And create a file in the newly created folder called ``food_list.html`` 512 | containing (copied from 513 | http://twitter.github.com/bootstrap/getting-started.html and changed to serve 514 | static media from Django's locations): 515 | 516 | .. code-block:: html+django 517 | 518 | 519 | 520 | 521 | Bootstrap 101 Template 522 | 523 | 524 | 525 | 526 |

Hello, world!

527 | 528 | 529 | 530 | 531 | 532 | This template needs some files from the *Twitter Bootstrap* project, so in your 533 | app folder, download twitter bootstrap static files, unzip and rename the 534 | directory to ``static``:: 535 | 536 | $ wget http://twitter.github.com/bootstrap/assets/bootstrap.zip 537 | $ unzip bootstrap.zip 538 | $ rm bootstrap.zip 539 | $ mv bootstrap static 540 | 541 | Have a look at the file structure there and compare to the explanations at 542 | http://twitter.github.com/bootstrap/getting-started.html. It should be alright. 543 | 544 | You need to stop and start the server again, as the new templates folder is 545 | only picked up at server restart. 546 | 547 | Now, refresh the web browser and see the page saying "Hello, world!". 548 | 549 | The real coding begins 550 | ---------------------- 551 | 552 | *We will now change the template. You can compare your work to the full example further down the page.* 553 | 554 | Add a ``div`` tag with class *container* around the ``h1`` and see how the page 555 | changes. Change the template by changing the *h1* tag and the *title*, and 556 | after the *h1* (but inside the div), display the contents of the 557 | ``object_list`` template variable we created above, like this: 558 | 559 | .. code-block:: html+django 560 | 561 |
    562 | {% for object in object_list %} 563 |
  • {{ object }}
  • 564 | {% endfor %} 565 |
566 | 567 | Refresh your browser and see. We want to see some details about the food we 568 | have created, but we do not know the addresses to these pages yet, so we will 569 | insert empty links (a href="") around the {{ object }}. Insert this instead of 570 | ``{{ object }}``, (inside the *li*-tag): 571 | 572 | .. code-block:: html+django 573 | 574 | {{ object }} 575 | 576 | Also add an empty link at the bottom of the page that will later be used for 577 | adding more food to our list. 578 | 579 | .. code-block:: html+django 580 | 581 | Add food 582 | 583 | .. image:: food_list.png 584 | 585 | The template should now look similar to this: 586 | 587 | .. code-block:: html+django 588 | 589 | 590 | 591 | 592 | Food 593 | 594 | 595 | 596 | 597 |
598 |

Food

599 | 600 |
    601 | {% for object in object_list %} 602 |
  • {{ object }}
  • 603 | {% endfor %} 604 |
605 | 606 | Add food 607 |
608 | 609 | 610 | 611 | 612 | 613 | 614 | A simpler view 615 | -------------- 616 | 617 | The view function we made earlier gives us full control over what happens. But 618 | it is long, and making a few of these requires a lot of typing. To make sure 619 | you *don't repeat yourself* too much, you can use the newer "Class based 620 | generic view"s instead. 621 | 622 | In *views.py*, remove the file contents and insert this instead:: 623 | 624 | from recipes.models import Food 625 | from django.views.generic import ListView 626 | 627 | class FoodListView(ListView): 628 | model = Food 629 | 630 | And the urls.py should import the new ``FoodListView`` instead of food_list, 631 | like this:: 632 | 633 | from recipes.views import FoodListView 634 | 635 | And the pattern should be changed to this:: 636 | 637 | url(r'^food/$', FoodListView.as_view(), name='food-list'), 638 | 639 | Here, instead of calling the view function directly, we are now calling the 640 | ``as_view`` function on the FoodListView class we just created. Our 641 | ``FoodListView`` does not define this ``as_view()`` function, but inherits it 642 | from the ``ListView`` model class of the ``django.views.generic`` module. 643 | 644 | Have a look in the browser. The functionality is the same, the code a bit 645 | shorter. You may need the old syntax from time to time, so you should know that 646 | an old and more flexible syntax exists. 647 | 648 | Your second view: Food details 649 | ============================== 650 | 651 | We want to get up a page for each food object, that may in the future list more 652 | details. In the *views.py*, append ``DetailView`` (comma separated) to the 653 | *django.views* import statement at the top, and add another class at the bottom 654 | of the file:: 655 | 656 | class FoodDetailView(DetailView): 657 | model = Food 658 | 659 | Add another pattern to the *urls.py* of the app, and remember to import 660 | FoodDetailView at the top (the same way as you imported DetailView above):: 661 | 662 | url(r'^food/(?P\d+)$', FoodDetailView.as_view(), name='food-detail'), 663 | 664 | The ```` part of the pattern says that you want to match the primary key 665 | field of the object. The primary key field is for all common cases the hidden 666 | auto-incremented numerical *id*. When this url pattern is matched to an 667 | incoming url, we have a match if the incoming url starts with the ``recipes/`` 668 | of the app, defined in the project's urls.py, then the next part has to be 669 | ``food/`` as defined here, and it has to *end with a number*. If we have a 670 | match, Django will run the inherited ``as_view`` function of the 671 | FoodDetailsView model, fetching the database record matching the incoming 672 | integer to the Food table's primary key. 673 | 674 | You see that the last parameter is "name". This is a short name to use when 675 | refering to links that may be long and have a lot of parameters. Another good 676 | thing about this is that we can change the urls without updating all the places 677 | where they are used. This concept is called *named urls* in Django, and this 678 | way, the url patterns are used both for **url pattern matching** *and* **link 679 | url generation**. 680 | 681 | In the template we created some moments ago, insert the name of the url you 682 | need into the address field, so that the line becomes: 683 | 684 | .. code-block:: html+django 685 | 686 |
  • {{ object }}
  • 687 | 688 | Here we send in object.id (the hidden primary key of the object), so that when 689 | it is clicked, it will get used the other way, as described above. 690 | 691 | When you have a look at the web browser now, you see by hovering the mouse over 692 | the links that they point somewhere. By clicking one of them, you will see we 693 | need to make another template. *templates/food_detail.html* is missing. 694 | 695 | .. image:: food_detail_template_missing.png 696 | 697 | In the innermost template folder, copy the template you already have to 698 | ``food_detail.html`` in the same folder. Change the new template to add a new 699 | *title*, *h1* and the *contents* itself. In the title and h1, you can use {{ 700 | object }}. This will make use of the already utilized 701 | ``__unicode__``-function. 702 | 703 | The contents is not too much fun as we do only have one field in the Food 704 | model, and the normally hidden *id*. Add a few ``

    ``-tags with the object id 705 | and name, and a link back to the list, using the *named url*, like this: 706 | 707 | .. code-block:: html+django 708 | 709 |

    Back to food list

    710 | 711 |

    {{ object.id }}

    712 |

    {{ object.name }}

    713 | 714 | You can be happy if the detailed page looks something like this when you 715 | refresh the browser: 716 | 717 | .. image:: food_detail.png 718 | 719 | Don't repeat yourself: Use a common base 720 | ---------------------------------------- 721 | 722 | When you look at the two templates, you see that there is a lot of common code 723 | in them. Adding even more templates, the contents will become unorganized and 724 | at last not look consistent. Not good. Create a new template *one folder level 725 | up*, relative to the two you have, called "base.html" with the common code, 726 | like this: 727 | 728 | .. code-block:: html+django 729 | 730 | 731 | 732 | 733 | {% block title %}Generic title{% endblock %} 734 | 735 | 736 | 737 | 738 |
    739 | {% block content %} 740 |

    Generic title

    741 | 742 | Nothing interesting yet 743 | 744 | {% endblock %} 745 |
    746 | 747 | 748 | 749 | 750 | 751 | You see some placeholder text in there, inside some blocks ``{% block content 752 | %}``. Blocks are made to be overridden in the templates extending them, so we 753 | will only see this placeholder text if we forget to override the blocks in the 754 | other templates. 755 | 756 | Now remove the common code from the other two templates and add a line at the 757 | top to tell them to **extend** the new base template. Then override the two 758 | blocks, title and content in both templates. The list template now looks like 759 | this: 760 | 761 | .. code-block:: html+django 762 | 763 | {% extends "base.html" %} 764 | 765 | {% block title %}Food list{% endblock %} 766 | 767 | {% block content %} 768 |

    Food list

    769 |
      770 | {% for object in object_list %} 771 |
    • {{ object }}
    • 772 | {% endfor %} 773 |
    774 | 775 | Add food 776 | {% endblock %} 777 | 778 | Now, the browser should look exactly the same for the two views. If you see the 779 | generic text of the base, then you do not override the blocks using the same 780 | names. 781 | 782 | Create more objects 783 | =================== 784 | 785 | Append ``CreateView`` to the django.views import at the top of *views.py*, and 786 | create a new view like:: 787 | 788 | class FoodCreateView(CreateView): 789 | model = Food 790 | 791 | In the *urls.py*, add the new FoodCreateView to the import at the top, and add a 792 | new url pattern:: 793 | 794 | url(r'^food/create$', FoodCreateView.as_view(), name='food-create'), 795 | 796 | It is good practice to have the more specific patterns above the patterns that 797 | tries to match url input. If the food details url was set up to take a word 798 | instead of a number as a parameter, it would also match "create" and would be 799 | trying to fetch a food object named "create". 800 | 801 | Now you can update the create link in the list template to use the new and 802 | named ``food-create``, like this: 803 | 804 | .. code-block:: html+django 805 | 806 | Add food 807 | 808 | Clicking the new link will also give an error about a missing template. 809 | 810 | .. image:: food_create_view_missing.png 811 | 812 | The error message tells us that "recipes/food_form.html" is missing. Create it 813 | and make it look similar to the other two templates, but with a form added to 814 | it: 815 | 816 | .. code-block:: html+django 817 | 818 | {% extends "base.html" %} 819 | 820 | {% block title %}Add food{% endblock %} 821 | 822 | {% block content %} 823 |

    Add food

    824 | 825 |
    826 | {{ form }} 827 | 828 |
    829 | {% endblock %} 830 | 831 | We haven't added any action or method parameters to the form at this time. The 832 | ``{{ form }}`` tag will let Django show the fields that represent the models. 833 | And we also have a standard submit button. Have a look at the form in the 834 | browser. 835 | 836 | .. image:: food_create_boring.png 837 | 838 | OK, but we can do better… 839 | 840 | Primary action button 841 | --------------------- 842 | 843 | To make it slightly nicer, add a ``class="btn btn-primary"`` to the submit 844 | button. Looks better? This is because of the styling we get from Twitter 845 | Bootstrap. 846 | 847 | .. image:: food_create_blue_button.png 848 | 849 | A more crispy form 850 | ------------------ 851 | 852 | We will also make the form layout a bit nicer with the third party **Crispy 853 | Forms** module. This will help us by adding useful `CSS`_ classes that will be 854 | styled by the bootstrap css rules. To INSTALLED_APPS in *setup.py*, add 855 | ``crispy_forms`` and install django-crispy-forms with pip:: 856 | 857 | pip install django-crispy-forms 858 | 859 | Below the extends line in the food_form template, add: 860 | 861 | .. code-block:: html+django 862 | 863 | {% load crispy_forms_tags %} 864 | 865 | And add the ``crispy`` filter to the form variable, like this: 866 | 867 | .. code-block:: html+django 868 | 869 |
    870 | {{ form|crispy }} 871 | 872 |
    873 | 874 | .. _CSS: http://en.wikipedia.org/Css 875 | 876 | .. image:: food_create_crispy.png 877 | 878 | It now looks good, but it won't work. 879 | 880 | Making the form post 881 | -------------------- 882 | 883 | Now, add a fruit name and click "Save". The url changes, but you are still on 884 | the same page. Our Django view will answer differently on GET and POST 885 | requests, but we did not tell the form to use the http POST method. Change the 886 | form definition to use the POST method: 887 | 888 | .. code-block:: html+django 889 | 890 |
    891 | 892 | If we try again, we will see another error, complaining about "Forbidden: CSRF 893 | verification failed. Request aborted.". *Cross site request forgery* is a well 894 | established mechanism to used to decide that a request originates from the same 895 | site. This is done by using the randomly generated ``SECRET`` variable in 896 | settings.py to generate a combination of characters that will be attached as 897 | hidden fields to all forms, and then be validated on the servers when the form 898 | is posted. All you have to do is to add a ``{% csrf_token %}`` to your form. 899 | Add this e.g. at the same line as the form definition tag, like this: 900 | 901 | .. code-block:: html+django 902 | 903 | {% csrf_token %} 904 | 905 | Now, try to save again. Another error! So much errors, so much to learn! 906 | 907 | .. image:: no_url_to_redirect_to.png 908 | 909 | This time Django complains about not knowing where to send you after the form 910 | has been parsed and your object saved. You would need to define either a 911 | ``success_url`` in the view, to tell it where to go, or you can let Django go 912 | back to the detailed view for the object. This is the default behaviour, as 913 | long as you have a ``get_absolute_url`` method defined in your model. Head over 914 | to models.py and add a method at the bottom of your Food class (on the same 915 | indentation level as ``__unicode__``):: 916 | 917 | @models.permalink 918 | def get_absolute_url(self): 919 | return ('food-detail', [self.id]) 920 | 921 | The ``@models.permalink`` gives a short and easier way to write a url than when 922 | calling ``reverse`` yourself. 923 | 924 | Now, go back and add a fruit and click save. Nice? If you now have two fruits 925 | with the same name, that is because your fruit got added even though your 926 | success link were missing. 927 | 928 | .. image:: two_of_the_same.png 929 | 930 | To be sure you will never register the same fruit twice, you can add 931 | ``unique=True`` within the definition of ``name`` in your model class, so the 932 | changed definition get something like:: 933 | 934 | name = models.CharField(max_length=20, unique=True) 935 | 936 | If you now try to add a food object that has the same name as one previously created, you should see an error message like this: 937 | 938 | .. image:: food_with_this_name_already_exists.png 939 | 940 | Now you know how to add a model and some views to list, see details or add new 941 | objects. 942 | 943 | More models 944 | =========== 945 | 946 | To be able to create recipes, we need at least two more models. A recipe model 947 | is obvious, where we can add ingredients and a description of how to use the 948 | ingredients. But how do we connect the recipes to the food objects? 949 | 950 | There is a way to connect two models together in a very generic way. This is 951 | called a "many to many relation". In this case that would be too simple, as we 952 | need to add properties to the intermediate table. With `ManyToMany`_ we would 953 | be able to say what ingredients we need, but now how much of what ingredient we 954 | need in the recipe. 955 | 956 | .. _ManyToMany: https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_many/ 957 | 958 | We need to say what Food object we will use, how much of it, and to what 959 | ingredient we want it added. When saying how much, we need to know the 960 | measurement, as "1 salt" is not very useful. 961 | 962 | We will first define the Recipe model. It will have a title, a description of 963 | unknown length, and a __unicode__ method as we have already seen. But wouldn't 964 | it be nice to have a nice looking url? From the news paper agencies (where 965 | Django was first created), we have gotten *slug*\ s: readable parts of a url 966 | that will be used to identify an object. We will add a slug field that will 967 | hold a nice *slugified* version of the object's title:: 968 | 969 | class Recipe(models.Model): 970 | title = models.CharField(max_length=80) 971 | slug = models.SlugField(max_length=80) 972 | description = models.TextField() 973 | 974 | def __unicode__(self): 975 | return self.title 976 | 977 | To connect the Recipe to the Food, we create a table to hold the references as 978 | well as the measurement fields:: 979 | 980 | class Ingredient(models.Model): 981 | recipe = models.ForeignKey(Recipe) 982 | food = models.ForeignKey(Food) 983 | amount = models.DecimalField(decimal_places=2, max_digits=4) 984 | measurement = models.SmallIntegerField(choices=MEASUREMENT_CHOICES) 985 | 986 | We have ``ForeignKey`` fields that connects the *Ingredient* to a *Food* object 987 | and a *Recipe object*. The *amount* is defined as a ``DecimalField`` and the 988 | *measurement* as a ``SmallIntegerField``. We could have created a table for all 989 | the different measurements available, but we want to see how to make 990 | *predefined choices*. The measurements will be saved as a number, but should be 991 | treated as a choice of strings all the way through the application. In the 992 | above model definition, we refer to ``MEASUREMENT_CHOICES`` which are not 993 | defined. Define some choices **above** the Ingredient model definition where it 994 | will be referred to:: 995 | 996 | MEASUREMENT_CHOICES = ( 997 | (1, "piece"), 998 | (2, "liter"), 999 | (3, "cup"), 1000 | (4, "tablespoon"), 1001 | (5, "teaspoon"), 1002 | ) 1003 | 1004 | Migrations, simple 1005 | ------------------ 1006 | 1007 | Now that we have defined new models, we should create and run a new migration 1008 | as well. To create a new migration, go up to the project level directory where 1009 | *manage.py* is and run:: 1010 | 1011 | python manage.py schemamigration --auto recipes 1012 | 1013 | And run it with:: 1014 | 1015 | python manage.py syncdb --migrate 1016 | 1017 | You may get into trouble here if you still have food objects with the same 1018 | name. Head over to http://localhost:8000/admin/recipes/food/ and delete the 1019 | duplicates. Then, try to run the *syncdb* command above again. If you now get an error about a table ``recipes_recipe`` already existing, you may need to run an SQL command manually to fix it, as stated by the top of the error message:: 1020 | 1021 | DROP TABLE "recipes_recipe"; DROP TABLE "recipes_ingredient"; 1022 | 1023 | This is because sqlite3 is not a very good database backend, but as it is easy 1024 | to develop with, and you do not have very important data in our development 1025 | database, it is really no problem. Where to run that SQL command:: 1026 | 1027 | python manage.py dbshell 1028 | 1029 | Should give you access to the configured database shell. Exit by pressing ``ctrl-d``. **NOTE** that you should only do that if you got this error. 1030 | 1031 | Extending the admin inteface 1032 | ---------------------------- 1033 | 1034 | Register the two new models with the admin interface, in ``recipes/admin.py``. 1035 | (Do not forget to update the import statement):: 1036 | 1037 | admin.site.register(Recipe) 1038 | admin.site.register(Ingredient) 1039 | 1040 | In the admin interface (at /admin), try to add a new recipe, e.g. *Pancakes*. 1041 | Insert "My Pancakes" as the title and "my-pancakes" as the slug. Try to 1042 | save without filling in the "description" field. Click *Save*. Form validations 1043 | will not let you save this without filling in a description. Or telling the 1044 | model that an empty description is OK, by adding ``blank=True`` to the 1045 | description field, like:: 1046 | 1047 | description = models.TextField(blank=True) 1048 | 1049 | That worked. Before adding ingredient objects, go back and add some more food 1050 | objects, like "egg", "milk", "salt" and "wheat flour". 1051 | 1052 | And then, add a new ingredient object. Choose "My Pancakes", "Milk", "0.5" 1053 | and "liter" and save. 1054 | 1055 | We get redirected back to the Ingredient list, and see that we need to add a 1056 | __unicode__ method to the ingredient class. Python has several ways to format a 1057 | string to look nice(REF). The first attempt is to add the method like this:: 1058 | 1059 | def __unicode__(self): 1060 | return "%f %s %s (%s)" % (self.amount, self.measurement, self.food, self.recipe) 1061 | 1062 | Here, we output a number which may contain decimals for the amount, a string 1063 | for the measurement and a string in parentheses for the recipe it belongs to. 1064 | 1065 | When refreshing the ingredient list page, you see that the ``%f`` gives a lot 1066 | of unneeded decimals. Change this to ``%.2g`` to allow at most two decimals. 1067 | 1068 | You also see that the line does not print out the measurement, only the 1069 | numerical id. So change the ``self.measurement`` to 1070 | ``self.get_measurement_display()`` to use a method that is dynamically 1071 | available to fields with choices. (In the documentation this is called 1072 | ``get_FIELD_display()``). 1073 | 1074 | Now we have been sort of exploiting the string representation of the object to 1075 | make it look nice in the admin interface. But instead of using the object's 1076 | string representation in a single cell in the table, you can define how to 1077 | represent the object in the admin interface. Replace the Ingredient line in 1078 | admin.py with this:: 1079 | 1080 | class IngredientAdmin(admin.ModelAdmin): 1081 | list_display = ('food', 'amount', 'measurement', 'recipe') 1082 | 1083 | admin.site.register(Ingredient, IngredientAdmin) 1084 | 1085 | Here, you also see that the measurement is listed using multiple columns. 1086 | 1087 | New views 1088 | ========= 1089 | 1090 | Now, everything looks nice in the admin interface, but we still do not want to 1091 | expose it to the users. We need to get similar functionality in our own views. 1092 | 1093 | We want to list all recipes, so you should add a RecipeListView and a 1094 | RecipeDetailView to *views.py*. Import the Recipe model in the first line, and 1095 | add two new views:: 1096 | 1097 | class RecipeListView(ListView): 1098 | model = Recipe 1099 | 1100 | class RecipeDetailView(DetailView): 1101 | model = Recipe 1102 | 1103 | Create two new url patterns like this to the *urls.py*, and remember to do the 1104 | correct import at the top:: 1105 | 1106 | url(r'^$', RecipeListView.as_view(), name='recipe-list'), 1107 | url(r'^(?P[-\w]+)$', RecipeDetailView.as_view(), name='recipe-detail'), 1108 | 1109 | The first will match the address "/recipes/". The second will match "/recipes/" 1110 | plus "a string containing numbers, letters, hyphens and underscores". This is 1111 | used to match the slug field we described earlier. The ``P`` actually 1112 | saves the value to a parameter named "slug", which is treated almost like an id 1113 | internally by Django. 1114 | 1115 | Now copy the template *food_list.html* to *recipe_list.html* in the same 1116 | folder, and modify the new recipe list to be useful to list recipes. Also get 1117 | the list to link to the recipe-detail url that you just created: 1118 | 1119 | .. code-block:: html+django 1120 | 1121 | {% url recipe-detail object.slug %} 1122 | 1123 | While you are at it, copy *food_detail.html* to *recipe_detail.html* and modify 1124 | that as well. The contents could be something like: 1125 | 1126 | .. code-block:: html+django 1127 | 1128 |

    {{ object.title }}

    1129 | 1130 |

    Back to recipe list

    1131 | 1132 |

    Ingredients

    1133 |
      1134 | {% for ingredient in object.ingredient_set.all %} 1135 |
    • {{ ingredient}}
    • 1136 | {% endfor %} 1137 |
    1138 | 1139 |

    Description

    1140 |

    {{ object.description }}

    1141 | 1142 | Here you see how we can list out the ingredients of the recipe. 1143 | 1144 | You should now be able to navigate between the list and the detailed recipe(s). 1145 | In the recipe_detail.html you just created, change the last line to add 1146 | ``|default:"No description"`` to print out a default value when the description 1147 | has not been added. In case you wonder, this is how it should look: 1148 | 1149 | .. code-block:: html+django 1150 | 1151 |

    {{ object.description|default:"No description" }}

    1152 | 1153 | Add recipes 1154 | ----------- 1155 | 1156 | Now, we will add a new view by doing it the other way around. Add or update the 1157 | linke at the bottom of the recipe_list.html. Like this: 1158 | 1159 | .. code-block:: html+django 1160 | 1161 | Add new 1162 | 1163 | Here, we point to a url pattern called recipe-create, and if you try to view 1164 | the recipe list now, you will get an error message telling you this, you are 1165 | using a link that is not defined. 1166 | 1167 | .. image:: no-reverse-match-recipe-create.png 1168 | 1169 | So head over to urls.py and add ``recipe-create`` *before* the 1170 | ``recipe-detail`` url (if you put it after, the recipe-detail will be reached 1171 | first, and you will try to fetch a recipe called "create"):: 1172 | 1173 | url(r'^new/$', RecipeCreateView.as_view(), name='recipe-create'), 1174 | 1175 | If you try to view the recipe-list in the browser now, you will see an error 1176 | message telling you that ``RecipeCreateView`` is not defined. 1177 | 1178 | .. image:: recipe-create-view-not-defined.png 1179 | 1180 | Add the missing import line, try again, and you will get an error message 1181 | telling you that it cannot find ``RecipeCreteView`` in *views.py*. 1182 | 1183 | .. image:: cannot-import-name-recipecreateview.png 1184 | 1185 | So, go ahead and create that simple function:: 1186 | 1187 | class RecipeCreateView(CreateView): 1188 | model = Recipe 1189 | 1190 | Try it in your browser. Yes, we now have a nice button. 1191 | 1192 | .. image:: recipe-list.png 1193 | 1194 | If you try clicking that new button, you will once again see the error about a 1195 | missing template. Even if this is a new template, the contents should look very 1196 | familiar. You can copy food_form.html to recipe_form.html and do just a few 1197 | modifications to get what you want: 1198 | 1199 | .. code-block:: html+django 1200 | 1201 | {% extends "base.html" %} 1202 | {% load crispy_forms_tags %} 1203 | 1204 | {% block title %}Add recipe{% endblock %} 1205 | 1206 | {% block content %} 1207 | 1208 |

    Add recipe

    1209 | 1210 | 1211 | {% csrf_token %} 1212 | {{ form|crispy }} 1213 | 1214 | 1215 | 1216 | {% endblock %} 1217 | 1218 | Now, you should see something useful in your browser. Try to create a simple 1219 | recipe, were you do not use too much time, as I now warn you that this will end 1220 | in an error. Yes, once again, Django complains about a missing ``success_url`` 1221 | - it does not know where to send us after the object is created. 1222 | 1223 | .. image:: improperly_configured_recipes_create.png 1224 | 1225 | You have already done this. Create a method in the Recipe model named 1226 | ``get_aboslute_url`` that will return the recipe-detail url:: 1227 | 1228 | @models.permalink 1229 | def get_absolute_url(self): 1230 | return ('recipe-detail', [self.slug]) 1231 | 1232 | You see how we use include the *slug* when creating this url, as we need that to 1233 | access the human readable url. 1234 | 1235 | Try to add another recipe, and see that everything is now working. 1236 | 1237 | Editing an object 1238 | ================= 1239 | 1240 | The way to edit an object is not too different from creating a new object. It 1241 | is in fact so similar that Django by default reuses the same template. As we 1242 | will see, one of the differences is how we need to identify the object we are 1243 | going to edit. 1244 | 1245 | To the recipe-detail template, add a link to a still undefined url 1246 | ``recipe-update``: 1247 | 1248 | .. code-block:: html+django 1249 | 1250 |

    Edit description

    1251 | 1252 | The url will contain the slug, like the create view:: 1253 | 1254 | url(r'^(?P[-\w]+)/update$', RecipeUpdateView.as_view(), name='recipe-updat e'), 1255 | 1256 | The view will not be very different from before, but you need to remember to 1257 | import ``UpdateView`` and code the view itself:: 1258 | 1259 | class RecipeUpdateView(UpdateView): 1260 | model = Recipe 1261 | 1262 | Now this should work without adding another template, as the *recipe_form.html* 1263 | will be used by both the create view and the update view. When you try to edit 1264 | the description, you will see that the template still says "Add recipe". 1265 | 1266 | Using a non-default template 1267 | ---------------------------- 1268 | 1269 | You could have chosen other texts that would have fit both a create form and an 1270 | update form, but we want to show how to use a non-default template. Copy the 1271 | *recipe_form.html* to *recipe_update_form.html*, change its contents so it says 1272 | "Change recipe" and set the ``template_name`` variable of the view to point to 1273 | the new template instead of silently pointing to the default:: 1274 | 1275 | class RecipeUpdateView(UpdateView): 1276 | model = Recipe 1277 | template_name = "recipes/recipe_update_form.html" 1278 | 1279 | Adding ingredients 1280 | ================== 1281 | 1282 | The last thing to do is to combine all of this to add, show and delete 1283 | ingredients. Start by adding a link to the recipe-list template where your 1284 | users can click to add ingredients: 1285 | 1286 | .. code-block:: html+django 1287 | 1288 |

    Add ingredient

    1289 | 1290 | You see that we need to send in the slug of the object so that we do not need 1291 | our users to choose this from a menu later. This slug is of course also part of 1292 | the needed url pattern:: 1293 | 1294 | url(r'^(?P[-\w]+)/add_ingredient/$', IngredientCreateView.as_view(), name='ingredient-create'), 1295 | 1296 | We first define the view as simple as possible:: 1297 | 1298 | class IngredientCreateView(CreateView): 1299 | model = Ingredient 1300 | 1301 | This will now work, except for the missing template, *ingredient_form.html*:: 1302 | 1303 | {% extends "base.html" %} 1304 | {% load crispy_forms_tags %} 1305 | 1306 | {% block title %}Add ingredient{% endblock %} 1307 | 1308 | {% block content %} 1309 | 1310 |

    Add ingredient

    1311 | 1312 |
    1313 | {% csrf_token %} 1314 | {{ form|crispy }} 1315 | 1316 |
    1317 | 1318 | {% endblock %} 1319 | 1320 | When you look at the form in your browser, you see that you can make it a 1321 | little bit simpler to use by taking away the "Recipe" form field. First, add a 1322 | method to the ``IngredientCreateView`` that will select initial values in our 1323 | form:: 1324 | 1325 | def get_initial(self, *args, **kwargs): 1326 | recipe = Recipe.objects.get(slug=self.kwargs['slug']) 1327 | return {'recipe': recipe} 1328 | 1329 | This will use the slug to fetch the corresponding ``Recipe`` object, and use 1330 | that to fill in the initial value of the ``recipe`` form field. Try it out and 1331 | see that it works. 1332 | 1333 | The next step is to hide the field from the user, as they should no longer need 1334 | to do anything to it. To hide the field, you need to define your own form. We 1335 | do this by creating a new file in the same folder as views.py called 1336 | *forms.py*. In this file, we define a new ``ModelForm`` (REF), a form that will 1337 | be based on the ``Ingredient`` model, and we override the form widget used to 1338 | show the recipe field:: 1339 | 1340 | from django.forms import ModelForm, HiddenInput 1341 | from recipes.models import Ingredient 1342 | 1343 | class IngredientForm(ModelForm): 1344 | 1345 | class Meta: 1346 | model = Ingredient 1347 | widgets = {'recipe': HiddenInput()} 1348 | 1349 | Now, have a look. Isn't it easier? Try to add some ingredients. Oh noes! 1350 | Another error! This time, we will actually define a success url, as we do not 1351 | want to show any details about "1 tablespoon of salt". We want to redirect back 1352 | to the recipe details instead. To the same view, add a method called 1353 | ``get_success_url`` that contains:: 1354 | 1355 | def get_success_url(self): 1356 | return reverse('recipe-detail', args=[self.kwargs['slug']]) 1357 | 1358 | Deleting objects 1359 | ---------------- 1360 | 1361 | You have probably done your fair share of testing now, and have accumulated a large amount of testdata. Some ingredients have been created that does not belong to some recipes, so we need to delete them. 1362 | 1363 | First, add a link to each ingredient row in the recipe detail template. It could say "delete" or be a little "x", but it should point to the url you name "ingredient-delete", and it should take in the object's slug and the ingredient's id:: 1364 | 1365 |
  • {{ ingredient }} x
  • 1366 | 1367 | Now, create the url pattern this points to:: 1368 | 1369 | url(r'^(?P[-\w]+)/remove_ingredient/(?P\d+)/$', IngredientDeleteView.as_view(), name='ingredient-delete'), 1370 | 1371 | This is probably the longest of them all as we use both the slug and the 1372 | ingredient's id field. You maybe wonder if we really need to pick up the slug 1373 | again, since the ingredient's id should be unique alone, but it is a nice 1374 | looking url, and it will save us from some work later. 1375 | 1376 | So, happily knowing what is going on, you bring up your browser and try to 1377 | delete one of the silly test ingredients, but what? An error? Missing an 1378 | *ingredient_confirm_delete.html* was maybe a bit unexpected. 1379 | 1380 | Delete confirmation 1381 | ------------------- 1382 | 1383 | The default delete view is doing the same thing as the create and update views 1384 | by showing a form on a GET request and processing the form on the form on a 1385 | POST request. 1386 | 1387 | There are several ways to circumvent the confirm_MODEL_delete.html templates, 1388 | by using a button in a small form, using javascript to send a POST request 1389 | instead of a get on the link clicking, redirecting from the GET to the POST… 1390 | but I think a delete confirmation page is a good habit, especially when listing 1391 | out related objects that would also be deleted. The *ingredient_confirm_delete* could look something like:: 1392 | 1393 | {% extends "base.html" %} 1394 | 1395 | {% block title %}Delete ingredient{% endblock %} 1396 | 1397 | {% block content %} 1398 |

    Delete ingredient

    1399 | 1400 |

    Really delete {{ object }} from {{ object.recipe }}?

    1401 | 1402 |

    It will be permanently lost

    1403 | 1404 |
    {% csrf_token %} 1405 | 1406 |
    1407 | 1408 | {% endblock %} 1409 | 1410 | # FIXME: se om det er noe vits med form-output 1411 | 1412 | The important thing is the delete button. Skipping the ``csrf_token`` will give 1413 | back the error about cross site scripting attacks again. 1414 | 1415 | You should really add a cancel button to the form as well to help the users, 1416 | bringing them back to the detail page without changing anything:: 1417 | 1418 | Cancel 1419 | 1420 | Now this is now a small form with a button and a small link. If you add some 1421 | css classes defined in the Twitter Bootstrap css, it can be a lot nicer. Add 1422 | ``class="btn"`` to the cancel link to style it like a button, and ``class="btn 1423 | btn-primary"`` to the delete button to make it look like a default action 1424 | button. 1425 | 1426 | Yes, this is nice an shiny, but the form is still not working. If you try it, 1427 | you'll see that we are missing a success-url. This time, we will just copy the 1428 | ``get_success_url`` we made in ``IngredientCreateView`` to 1429 | ``IngredientDeleteView`` to get the same redirect back to the 1430 | ``recipe-detail``:: 1431 | 1432 | def get_success_url(self): 1433 | return reverse('recipe-detail', args=[self.kwargs['slug']]) 1434 | 1435 | Now, this looks better, and redirects us to the recipe we deleted the 1436 | ingredient from. Just to show off, we could replace the delete link on the 1437 | recipe detail view with an icon from Twitter Bootstrap, by adding an 1438 | ````-tag with a class representing the icon we want to use ("icon-remove") 1439 | from http://twitter.github.com/bootstrap/base-css.html#icons:: 1440 | 1441 |
  • {{ ingredient}}
  • 1442 | 1443 | Easier editing with Markdown 1444 | ---------------------------- 1445 | 1446 | Try to edit the description of a recipe and save it. The description of a 1447 | recipe will probably consist of several steps on a way to the finished meal, 1448 | and you would probably want to put these steps in several paragraphs or a list. 1449 | As you probably guess, you would need to type html to get this nice looking. 1450 | 1451 | There is a filter called "markdown" filter that will take a more simpler made 1452 | text and convert it to html for you (REF). To the description field in the 1453 | recipe-detail template, add ``|markdown`` between ``description`` and 1454 | ``|default``, like this:: 1455 | 1456 |

    {{ object.description|markdown|default:"No description" }}

    1457 | 1458 | You shouldn't be surprised that this will not work. The error message should 1459 | tell you that Django does not understand "markdown". You need to load a module 1460 | where "markdown" is defined. On line two of the file, load the markup filters:: 1461 | 1462 | {% load markup %} 1463 | 1464 | This still does not work, because you also need to have a markdown library 1465 | installed which this filter will contact to parse the text. Head over to a 1466 | terminal where your virtualenv is activated, and install markdown using Python 1467 | package installer, Pip:: 1468 | 1469 | pip install markdown 1470 | 1471 | You will also need to tell Django to actually load this file in settings.py. In the INSTALLED_APPS section, add:: 1472 | 1473 | 'django.contrib.markup', 1474 | 1475 | You do not have an easy way to go between the recipe section and the food 1476 | section of your website. What about using a fancy top menu from Twitter 1477 | Bootstrap http://twitter.github.com/bootstrap/components.html#navbar? In 1478 | "base.html" template (one level up from the other templates), add a this inside 1479 | the "container" div, before the "content" block: 1480 | 1481 | .. code-block:: html 1482 | 1483 | 1492 | 1493 | Future sections? 1494 | ================ 1495 | 1496 | - debugging with ipython, pdb, web error 1497 | - unit testing 1498 | - authentication 1499 | 1500 | 1501 | Indices and tables 1502 | ================== 1503 | 1504 | * :ref:`genindex` 1505 | * :ref:`modindex` 1506 | * :ref:`search` 1507 | 1508 | 1509 | -------------------------------------------------------------------------------- /doc/it_worked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/it_worked.png -------------------------------------------------------------------------------- /doc/no-reverse-match-recipe-create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/no-reverse-match-recipe-create.png -------------------------------------------------------------------------------- /doc/no_admin_tables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/no_admin_tables.png -------------------------------------------------------------------------------- /doc/no_module_named_urls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/no_module_named_urls.png -------------------------------------------------------------------------------- /doc/no_url_to_redirect_to.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/no_url_to_redirect_to.png -------------------------------------------------------------------------------- /doc/page_not_found.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/page_not_found.png -------------------------------------------------------------------------------- /doc/recipe-create-view-not-defined.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/recipe-create-view-not-defined.png -------------------------------------------------------------------------------- /doc/recipe-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/recipe-list.png -------------------------------------------------------------------------------- /doc/two_of_the_same.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/two_of_the_same.png -------------------------------------------------------------------------------- /doc/user_friendly_food_objects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigurdga/django-by-errors/01b59ee420ea7ffb864b9da2f4183f4f5067070d/doc/user_friendly_food_objects.png --------------------------------------------------------------------------------