Browse Source

Beefed up the tutorial

pull/1638/head
Armin Ronacher 15 years ago
parent
commit
6dd92ae4b3
  1. 2
      docs/patterns.rst
  2. 8
      docs/quickstart.rst
  3. 157
      docs/testing.rst
  4. 188
      docs/tutorial.rst
  5. 8
      examples/flaskr/flaskr_tests.py
  6. 33
      examples/flaskr/static/style.css
  7. 2
      examples/flaskr/templates/show_entries.html

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
`````````````

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
-------

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/

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.

8
examples/flaskr/flaskr_tests.py

@ -33,6 +33,11 @@ class FlaskrTestCase(unittest.TestCase):
# 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 @@ class FlaskrTestCase(unittest.TestCase):
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>',

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; }

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:

Loading…
Cancel
Save