Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Beefed up the tutorial

  • Loading branch information...
commit 6dd92ae4b32c336564231e10db12458a8b9261ca 1 parent 1246f40
@mitsuhiko authored
View
2  docs/patterns.rst
@@ -40,6 +40,8 @@ So here a simple example how you can use SQLite 3 with Flask::
g.db.close()
return response
+.. _easy-querying:
+
Easy Querying
`````````````
View
8 docs/quickstart.rst
@@ -89,6 +89,14 @@ Or pass it to run::
Both will have exactly the same effect.
+.. admonition:: Attention
+
+ The interactive debugger however does not work in forking environments
+ which makes it nearly impossible to use on production servers but the
+ debugger still allows the execution of arbitrary code which makes it a
+ major security risk and **must never be used on production machines**
+ because of that.
+
Routing
-------
View
157 docs/testing.rst
@@ -22,72 +22,78 @@ installation.
The Application
---------------
-First we need an application to test for functionality. Let's start
-simple with a Hello World application (`hello.py`)::
+First we need an application to test for functionality. For the testing
+we will use the application from the :ref:`tutorial`. If you don't have
+that application yet, get the sources from `the examples`_.
- from flask import Flask, render_template_string
- app = Flask(__name__)
-
- @app.route('/')
- @app.route('/<name>')
- def hello(name='World'):
- return render_template_string('''
- <!doctype html>
- <title>Hello {{ name }}!</title>
- <h1>Hello {{ name }}!</h1>
- ''', name=name)
+.. _the examples:
+ http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/
The Testing Skeleton
--------------------
In order to test that, we add a second module (
-`hello_tests.py`) and create a unittest skeleton there::
+`flaskr_tests.py`) and create a unittest skeleton there::
import unittest
- import hello
+ import flaskr
+ import tempfile
- class HelloWorldTestCase(unittest.TestCase):
+ class FlaskrTestCase(unittest.TestCase):
def setUp(self):
- self.app = hello.app.test_client()
+ self.db = tempfile.NamedTemporaryFile()
+ self.app = flaskr.app.test_client()
+ flaskr.DATABASE = self.db.name
+ flaskr.init_db()
if __name__ == '__main__':
unittest.main()
-The code in the `setUp` function creates a new test client. That function
-is called before each individual test function. What the test client does
-for us is giving us a simple interface to the application. We can trigger
-test requests to the application and the client will also keep track of
-cookies for us.
+The code in the `setUp` function creates a new test client and initialize
+a new database. That function is called before each individual test function.
+What the test client does for us is giving us a simple interface to the
+application. We can trigger test requests to the application and the
+client will also keep track of cookies for us.
+
+Because SQLite3 is filesystem based we can easily use the tempfile module
+to create a temporary database and initialize it. Just make sure that you
+keep a reference to the :class:`~tempfile.NamedTemporaryFile` around (we
+store it as `self.db` because of that) so that the garbage collector does
+not remove that object and with it the database from the filesystem.
If we now run that testsuite, we should see the following output::
- $ python hello_tests.py
+ $ python flaskr_tests.py
----------------------------------------------------------------------
Ran 0 tests in 0.000s
OK
-Even though it did not run any tests, we already know that our hello
+Even though it did not run any tests, we already know that our flaskr
application is syntactically valid, otherwise the import would have died
with an exception.
The First Test
--------------
-Now we can add the first test. Let's check that the application greets us
-with "Hello World" if we access it on ``/``. For that we modify our
-created test case class so that it looks like this::
+Now we can add the first test. Let's check that the application shows
+"No entries here so far" if we access the root of the application (``/``).
+For that we modify our created test case class so that it looks like
+this::
- class HelloWorldTestCase(unittest.TestCase):
+ class FlaskrTestCase(unittest.TestCase):
def setUp(self):
- self.app = hello.app.test_client()
+ self.db = tempfile.NamedTemporaryFile()
+ self.app = flaskr.app.test_client()
+ flaskr.DATABASE = self.db.name
+ flaskr.init_db()
- def test_hello_world(self):
+ def test_empty_db(self):
rv = self.app.get('/')
- assert 'Hello World!' in rv.data
+ assert 'No entries here so far' in rv.data
Test functions begin with the word `test`. Every function named like that
will be picked up automatically. By using `self.app.get` we can send an
@@ -95,22 +101,87 @@ HTTP `GET` request to the application with the given path. The return
value will be a :class:`~flask.Flask.response_class` object. We can now
use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the
return value (as string) from the application. In this case, we ensure
-that ``'Hello World!'`` is part of the output.
+that ``'No entries here so far'`` is part of the output.
+
+Run it again and you should see one passing test::
+
+ $ python flaskr_tests.py
+ .
+ ----------------------------------------------------------------------
+ Ran 1 test in 0.034s
+
+ OK
+
+Of course you can submit forms with the test client as well which we will
+use now to log our user in.
+
+Logging In and Out
+------------------
+
+The majority of the functionality of our application is only available for
+the administration user. So we need a way to log our test client into the
+application and out of it again. For that we fire some requests to the
+login and logout pages with the required form data (username and
+password). Because the login and logout pages redirect, we tell the
+client to `follow_redirects`.
+
+Add the following two methods do your `FlaskrTestCase` class::
+
+ def login(self, username, password):
+ return self.app.post('/login', data=dict(
+ username=username,
+ password=password
+ ), follow_redirects=True)
-Run it again and you should see one passing test. Let's add a second test
-here::
+ def logout(self):
+ return self.app.get('/logout', follow_redirects=True)
- def test_hello_name(self):
- rv = self.app.get('/Peter')
- assert 'Hello Peter!' in rv.data
+Now we can easily test if logging in and out works and that it fails with
+invalid credentials. Add this as new test to the class::
+
+ def test_login_logout(self):
+ rv = self.login(flaskr.USERNAME, flaskr.PASSWORD)
+ assert 'You were logged in' in rv.data
+ rv = self.logout()
+ assert 'You were logged out' in rv.data
+ rv = self.login(flaskr.USERNAME + 'x', flaskr.PASSWORD)
+ assert 'Invalid username' in rv.data
+ rv = self.login(flaskr.USERNAME, flaskr.PASSWORD + 'x')
+ assert 'Invalid password' in rv.data
+
+Test Adding Messages
+--------------------
+
+Now we can also test that adding messages works. Add a new test method
+like this::
+
+ def test_messages(self):
+ self.login(flaskr.USERNAME, flaskr.PASSWORD)
+ rv = self.app.post('/add', data=dict(
+ title='<Hello>',
+ text='<strong>HTML</strong> allowed here'
+ ), follow_redirects=True)
+ assert 'No entries here so far' not in rv.data
+ self.login(flaskr.USERNAME, flaskr.PASSWORD)
+ assert '&lt;Hello&gt' in rv.data
+ assert '<strong>HTML</strong> allowed here' in rv.data
+
+Here we also check that HTML is allowed in the text but not in the title
+which is the intended behavior.
+
+Running that should now give us three passing tests::
+
+ $ python flaskr_tests.py
+ ...
+ ----------------------------------------------------------------------
+ Ran 3 tests in 0.332s
+
+ OK
-Of course you can submit forms with the test client as well. For that and
-other features of the test client, check the documentation of the Werkzeug
-test :class:`~werkzeug.Client` and the tests of the MiniTwit example
-application:
+For more complex tests with headers and status codes, check out the
+`MiniTwit Example`_ from the sources. That one contains a larger test
+suite.
-- Werkzeug Test :class:`~werkzeug.Client`
-- `MiniTwit Example`_
.. _MiniTwit Example:
http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/
View
188 docs/tutorial.rst
@@ -31,6 +31,13 @@ less web-2.0-ish name ;) Basically we want it to do the following things:
3. the page shows all entries so far in reverse order (newest on top) and
the user can add new ones from there if logged in.
+We will be using SQlite3 directly for that application because it's good
+enough for an application of that size. For larger applications however
+it makes a lot of sense to use `SQLAlchemy`_ that handles database
+connections in a more intelligent way, allows you to target different
+relational databases at once and more. You might also want to consider
+one of the popular NoSQL databases if your data is more suited for those.
+
Here a screenshot from the final application:
.. image:: _static/flaskr.png
@@ -38,6 +45,8 @@ Here a screenshot from the final application:
:class: screenshot
:alt: screenshot of the final application
+.. _SQLAlchemy: http://www.sqlalchemy.org/
+
Step 0: Creating The Folders
----------------------------
@@ -50,7 +59,13 @@ application::
The `flaskr` folder is not a python package, but just something where we
drop our files. Directly into this folder we will then put our database
-schema as well as main module in the following steps.
+schema as well as main module in the following steps. The files inside
+the `static` folder are available to users of the application via `HTTP`.
+This is the place where css and javascript files go. Inside the
+`templates` folder Flask will look for `Jinja2`_ templates. Drop all the
+templates there.
+
+.. _Jinja2: http://jinja.pocoo.org/2/
Step 1: Database Schema
-----------------------
@@ -79,12 +94,18 @@ Step 2: Application Setup Code
Now that we have the schema in place we can create the application module.
Let's call it `flaskr.py` inside the `flaskr` folder. For starters we
-will add the imports we will need as well as the config section::
+will add the imports we will need as well as the config section. For
+small applications it's a possibility to drop the configuration directly
+into the module which we will be doing here. However a cleaner solution
+would be to create a separate `.ini` or `.py` file and load that or import
+the values from there.
+
+::
# all the imports
import sqlite3
- from flask import Flask, request, session, g, redirect, url_for, abort, \
- render_template, flash
+ from flask import Flask, request, session, g, redirect, url_for, \
+ abort, render_template, flash
# configuration
DATABASE = '/tmp/flaskr.db'
@@ -93,17 +114,25 @@ will add the imports we will need as well as the config section::
USERNAME = 'admin'
PASSWORD = 'default'
-The `with_statement` and :func:`~contextlib.closing` function are used to
-make dealing with the database connection easier later on for setting up
-the initial database. Next we can create our actual application and
-initialize it with the config::
+Next we can create our actual application and initialize it with the
+config::
# create our little application :)
app = Flask(__name__)
app.secret_key = SECRET_KEY
app.debug = DEBUG
-We can also add a method to easily connect to the database sepcified::
+The `secret_key` is needed to keep the client-side sessions secure.
+Choose that key wisely and as hard to guess and complex as possible. The
+debug flag enables or disables the interactive debugger. Never leave
+debug mode activated in a production system because it will allow users to
+executed code on the server!
+
+We also add a method to easily connect to the database specified. That
+can be used to open a connection on request and also from the interactive
+Python shell or a script. This will come in handy later
+
+::
def connect_db():
return sqlite3.connect(DATABASE)
@@ -114,6 +143,11 @@ server if we run that file as standalone application::
if __name__ == '__main__':
app.run()
+With that out of the way you should be able to start up the application
+without problems. When you head over to the server you will get an 404
+page not found error because we don't have any views yet. But we will
+focus on that a little later. First we should get the database working.
+
.. admonition:: Troubleshooting
If you notice later that the browser cannot connect to the server
@@ -125,11 +159,6 @@ server if we run that file as standalone application::
default and not every browser is happy with that. This forces IPv4
usage.
-With that out of the way you should be able to start up the application
-without problems. When you head over to the server you will get an 404
-page not found error because we don't have any views yet. But we will
-focus on that a little later. First we should get the database working.
-
Step 3: Creating The Database
-----------------------------
@@ -159,7 +188,8 @@ first (`__future__` imports must be the very first import)::
from contextlib import closing
Next we can create a function called `init_db` that initializes the
-database::
+database. For this we can use the `connect_db` function we defined
+earlier. Just add that function below the `connect_db` function::
def init_db():
with closing(connect_db()) as db:
@@ -167,21 +197,26 @@ database::
db.cursor().executescript(f.read())
db.commit()
+The :func:`~contextlib.closing` helper function allows us to keep a
+connection open for the duration of the `with` block. The
+:func:`~flask.Flask.open_resource` method of the application object
+supports that functionality out of the box, so it can be used in the
+`with` block directly. This function opens a file from the resource
+location (your `flaskr` folder) and allows you to read from it. We are
+using this here to execute a script on the database connection.
+
+When we connect to a database we get a connection object (here called
+`db`) that can give us a cursor. On that cursor there is a method to
+execute a complete script. Finally we only have to commit the changes.
+SQLite 3 and other transactional databases will not commit unless you
+explicitly tell it to.
+
Now it is possible to create a database by starting up a Python shell and
importing and calling that function::
>>> from flaskr import init_db
>>> init_db()
-The :meth:`~flask.Flask.open_resource` function opens a file from the
-resource location (your flaskr folder) and allows you to read from it. We
-are using this here to execute a script on the database connection.
-
-When we connect to a database we get a connection object (here called
-`db`) that can give us a cursor. On that cursor there is a method to
-execute a complete script. Finally we only have to commit the changes and
-close the transaction.
-
Step 4: Request Database Connections
------------------------------------
@@ -225,7 +260,16 @@ view functions. We will need for of them:
Show Entries
````````````
-This view shows all the entries stored in the database::
+This view shows all the entries stored in the database. It listens on the
+root of the application and will select title and text from the database.
+The one with the highest id (the newest entry) on top. The rows returned
+from the cursor are tuples with the columns ordered like specified in the
+select statement. This is good enough for small applications like here,
+but you might want to convert them into a dict. If you are interested how
+to do that, check out the :ref:`easy-querying` example.
+
+The view function will pass the entries as dicts to the
+`show_entries.html` template and return the rendered one::
@app.route('/')
def show_entries():
@@ -238,7 +282,9 @@ Add New Entry
This view lets the user add new entries if he's logged in. This only
responds to `POST` requests, the actual form is shown on the
-`show_entries` page::
+`show_entries` page. If everything worked out well we will
+:func:`~flask.flash` an information message to the next request and
+redirect back to the `show_entries` page::
@app.route('/add', methods=['POST'])
def add_entry():
@@ -250,10 +296,19 @@ responds to `POST` requests, the actual form is shown on the
flash('New entry was successfully posted')
return redirect(url_for('show_entries'))
+Note that we check that the user is logged in here (the `logged_in` key is
+present in the session and `True`).
+
Login and Logout
````````````````
-These functions are used to sign the user in and out::
+These functions are used to sign the user in and out. Login checks the
+username and password against the ones from the configuration and sets the
+`logged_in` key in the session. If the user logged in successfully that
+key is set to `True` and the user is redirected back to the `show_entries`
+page. In that case also a message is flashed that informs the user he or
+she was logged in successfully. If an error occoured the template is
+notified about that and the user asked again::
@app.route('/login', methods=['GET', 'POST'])
def login():
@@ -269,6 +324,15 @@ These functions are used to sign the user in and out::
return redirect(url_for('show_entries'))
return render_template('login.html', error=error)
+The logout function on the other hand removes that key from the session
+again. We use a neat trick here: if you use the :meth:`~dict.pop` method
+of the dict and pass a second parameter to it (the default) the method
+will delete the key from the dictionary if present or do nothing when that
+key was not in there. This is helpful because we don't have to check in
+that case if the user was logged in.
+
+::
+
@app.route('/logout')
def logout():
session.pop('logged_in', None)
@@ -279,13 +343,32 @@ Step 6: The Templates
---------------------
Now we should start working on the templates. If we request the URLs now
-we would only get an exception that Flask cannot find the templates.
+we would only get an exception that Flask cannot find the templates. The
+templates are using `Jinja2`_ syntax and have autoescaping enabled by
+default. This means that unless you mark a value in the code with
+:class:`~flask.Markup` or with the ``|safe`` filter in the template,
+Jinja2 will ensure that special characters such as ``<`` or ``>`` are
+escaped with their XML equivalents.
+
+We are also using template inheritance which makes it possible to reuse
+the layout of the website in all pages.
Put the following templates into the `templates` folder:
layout.html
```````````
+This template contains the HTML skeleton, the header and a link to log in
+(or log out if the user was already logged in). It also displays the
+flashed messages if there are any. The ``{% block body %}`` block can be
+replaced by a block of the same name (``body``) in a child template.
+
+The :class:`~flask.session` dict is available in the template as well and
+you can use that to check if the user is logged in or not. Note that in
+Jinja you can access missing attributes and items of objects / dicts which
+makes the following code work, even if there is no ``'logged_in'`` key in
+the session:
+
.. sourcecode:: html+jinja
<!doctype html>
@@ -309,11 +392,17 @@ layout.html
show_entries.html
`````````````````
+This template extends the `layout.html` template from above to display the
+messages. Note that the `for` loop iterates over the messages we passed
+in with the :func:`~flask.render_template` function. We also tell the
+form to submit to your `add_entry` function and use `POST` as `HTTP`
+method:
+
.. sourcecode:: html+jinja
{% extends "layout.html" %}
{% block body %}
- {% if g.logged_in %}
+ {% if session.logged_in %}
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
<dl>
<dt>Title:
@@ -336,6 +425,9 @@ show_entries.html
login.html
``````````
+Finally the login template which basically just displays a form to allow
+the user to login:
+
.. sourcecode:: html+jinja
{% extends "layout.html" %}
@@ -352,3 +444,41 @@ login.html
</dl>
</form>
{% endblock %}
+
+Step 7: Adding Style
+--------------------
+
+Now that everything else works, it's time to add some style to the
+application. Just create a stylesheet called `style.css` in the `static`
+folder we created before:
+
+.. sourcecode:: css
+
+ body { font-family: sans-serif; background: #eee; }
+ a, h1, h2 { color: #377BA8; }
+ h1, h2 { font-family: 'Georgia', serif; margin: 0; }
+ h1 { border-bottom: 2px solid #eee; }
+ h2 { font-size: 1.2em; }
+
+ .page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
+ padding: 0.8em; background: white; }
+ .entries { list-style: none; margin: 0; padding: 0; }
+ .entries li { margin: 0.8em 1.2em; }
+ .entries li h2 { margin-left: -1em; }
+ .add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
+ .add-entry dl { font-weight: bold; }
+ .metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
+ margin-bottom: 1em; background: #fafafa; }
+ .flash { background: #CEE5F5; padding: 0.5em;
+ border: 1px solid #AACBE2; }
+ .error { background: #F0D6D6; padding: 0.5em; }
+
+Bonus: Testing the Application
+-------------------------------
+
+Now that you have finished the application and everything works as
+expected, it's probably not the best idea to add automated tests to
+simplify modifications in the future. The application above is used as a
+basic example of how to perform unittesting in the :ref:`testing` section
+of the documentation. Go there to see how easy it is to test Flask
+applications.
View
8 examples/flaskr/flaskr_tests.py
@@ -33,6 +33,11 @@ def logout(self):
# testing functions
+ def test_empty_db(self):
+ """Start with a blank database."""
+ rv = self.app.get('/')
+ assert 'No entries here so far' in rv.data
+
def test_login_logout(self):
"""Make sure login and logout works"""
rv = self.login(flaskr.USERNAME, flaskr.PASSWORD)
@@ -46,9 +51,6 @@ def test_login_logout(self):
def test_messages(self):
"""Test that messages work"""
- # start with a blank state
- rv = self.app.get('/')
- assert 'No entries here so far' in rv.data
self.login(flaskr.USERNAME, flaskr.PASSWORD)
rv = self.app.post('/add', data=dict(
title='<Hello>',
View
33 examples/flaskr/static/style.css
@@ -1,17 +1,18 @@
-body { font-family: sans-serif; background: #eee; }
-a, h1, h2 { color: #377BA8; }
-h1, h2 { font-family: 'Georgia', serif; margin: 0; }
-h1 { border-bottom: 2px solid #eee; }
-h2 { font-size: 1.2em; }
+body { font-family: sans-serif; background: #eee; }
+a, h1, h2 { color: #377BA8; }
+h1, h2 { font-family: 'Georgia', serif; margin: 0; }
+h1 { border-bottom: 2px solid #eee; }
+h2 { font-size: 1.2em; }
-div.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
- padding: 0.8em; background: white; }
-ul.entries { list-style: none; margin: 0; padding: 0; }
-ul.entries li { margin: 0.8em 1.2em; }
-ul.entries li h2 { margin-left: -1em; }
-form.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
-form.add-entry dl { font-weight: bold; }
-div.metanav { text-align: right; font-size: 0.8em; background: #fafafa;
- padding: 0.3em; margin-bottom: 1em; }
-div.flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; }
-p.error { background: #F0D6D6; padding: 0.5em; }
+.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
+ padding: 0.8em; background: white; }
+.entries { list-style: none; margin: 0; padding: 0; }
+.entries li { margin: 0.8em 1.2em; }
+.entries li h2 { margin-left: -1em; }
+.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
+.add-entry dl { font-weight: bold; }
+.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
+ margin-bottom: 1em; background: #fafafa; }
+.flash { background: #CEE5F5; padding: 0.5em;
+ border: 1px solid #AACBE2; }
+.error { background: #F0D6D6; padding: 0.5em; }
View
2  examples/flaskr/templates/show_entries.html
@@ -1,6 +1,6 @@
{% extends "layout.html" %}
{% block body %}
- {% if g.logged_in %}
+ {% if session.logged_in %}
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
<dl>
<dt>Title:
Please sign in to comment.
Something went wrong with that request. Please try again.