forked from orbit-oss/flask
Beefed up the tutorial
This commit is contained in:
parent
1246f4088a
commit
6dd92ae4b3
7 changed files with 306 additions and 92 deletions
|
|
@ -40,6 +40,8 @@ So here a simple example how you can use SQLite 3 with Flask::
|
||||||
g.db.close()
|
g.db.close()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
.. _easy-querying:
|
||||||
|
|
||||||
Easy Querying
|
Easy Querying
|
||||||
`````````````
|
`````````````
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,14 @@ Or pass it to run::
|
||||||
|
|
||||||
Both will have exactly the same effect.
|
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
|
Routing
|
||||||
-------
|
-------
|
||||||
|
|
|
||||||
157
docs/testing.rst
157
docs/testing.rst
|
|
@ -22,72 +22,78 @@ installation.
|
||||||
The Application
|
The Application
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
First we need an application to test for functionality. Let's start
|
First we need an application to test for functionality. For the testing
|
||||||
simple with a Hello World application (`hello.py`)::
|
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
|
.. _the examples:
|
||||||
app = Flask(__name__)
|
http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/
|
||||||
|
|
||||||
@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 Testing Skeleton
|
The Testing Skeleton
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
In order to test that, we add a second module (
|
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 unittest
|
||||||
import hello
|
import flaskr
|
||||||
|
import tempfile
|
||||||
|
|
||||||
class HelloWorldTestCase(unittest.TestCase):
|
class FlaskrTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
||||||
The code in the `setUp` function creates a new test client. That function
|
The code in the `setUp` function creates a new test client and initialize
|
||||||
is called before each individual test function. What the test client does
|
a new database. That function is called before each individual test function.
|
||||||
for us is giving us a simple interface to the application. We can trigger
|
What the test client does for us is giving us a simple interface to the
|
||||||
test requests to the application and the client will also keep track of
|
application. We can trigger test requests to the application and the
|
||||||
cookies for us.
|
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::
|
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
|
Ran 0 tests in 0.000s
|
||||||
|
|
||||||
OK
|
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
|
application is syntactically valid, otherwise the import would have died
|
||||||
with an exception.
|
with an exception.
|
||||||
|
|
||||||
The First Test
|
The First Test
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
Now we can add the first test. Let's check that the application greets us
|
Now we can add the first test. Let's check that the application shows
|
||||||
with "Hello World" if we access it on ``/``. For that we modify our
|
"No entries here so far" if we access the root of the application (``/``).
|
||||||
created test case class so that it looks like this::
|
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):
|
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('/')
|
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
|
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
|
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
|
value will be a :class:`~flask.Flask.response_class` object. We can now
|
||||||
use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the
|
use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the
|
||||||
return value (as string) from the application. In this case, we ensure
|
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. Let's add a second test
|
Run it again and you should see one passing test::
|
||||||
here::
|
|
||||||
|
|
||||||
def test_hello_name(self):
|
$ python flaskr_tests.py
|
||||||
rv = self.app.get('/Peter')
|
.
|
||||||
assert 'Hello Peter!' in rv.data
|
----------------------------------------------------------------------
|
||||||
|
Ran 1 test in 0.034s
|
||||||
|
|
||||||
Of course you can submit forms with the test client as well. For that and
|
OK
|
||||||
other features of the test client, check the documentation of the Werkzeug
|
|
||||||
test :class:`~werkzeug.Client` and the tests of the MiniTwit example
|
Of course you can submit forms with the test client as well which we will
|
||||||
application:
|
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)
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
return self.app.get('/logout', follow_redirects=True)
|
||||||
|
|
||||||
|
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 '<Hello>' 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
|
||||||
|
|
||||||
|
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:
|
.. _MiniTwit Example:
|
||||||
http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/
|
http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
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:
|
Here a screenshot from the final application:
|
||||||
|
|
||||||
.. image:: _static/flaskr.png
|
.. image:: _static/flaskr.png
|
||||||
|
|
@ -38,6 +45,8 @@ Here a screenshot from the final application:
|
||||||
:class: screenshot
|
:class: screenshot
|
||||||
:alt: screenshot of the final application
|
:alt: screenshot of the final application
|
||||||
|
|
||||||
|
.. _SQLAlchemy: http://www.sqlalchemy.org/
|
||||||
|
|
||||||
Step 0: Creating The Folders
|
Step 0: Creating The Folders
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
|
|
@ -50,7 +59,13 @@ application::
|
||||||
|
|
||||||
The `flaskr` folder is not a python package, but just something where we
|
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
|
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
|
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.
|
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
|
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
|
# all the imports
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from flask import Flask, request, session, g, redirect, url_for, abort, \
|
from flask import Flask, request, session, g, redirect, url_for, \
|
||||||
render_template, flash
|
abort, render_template, flash
|
||||||
|
|
||||||
# configuration
|
# configuration
|
||||||
DATABASE = '/tmp/flaskr.db'
|
DATABASE = '/tmp/flaskr.db'
|
||||||
|
|
@ -93,17 +114,25 @@ will add the imports we will need as well as the config section::
|
||||||
USERNAME = 'admin'
|
USERNAME = 'admin'
|
||||||
PASSWORD = 'default'
|
PASSWORD = 'default'
|
||||||
|
|
||||||
The `with_statement` and :func:`~contextlib.closing` function are used to
|
Next we can create our actual application and initialize it with the
|
||||||
make dealing with the database connection easier later on for setting up
|
config::
|
||||||
the initial database. Next we can create our actual application and
|
|
||||||
initialize it with the config::
|
|
||||||
|
|
||||||
# create our little application :)
|
# create our little application :)
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = SECRET_KEY
|
app.secret_key = SECRET_KEY
|
||||||
app.debug = DEBUG
|
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():
|
def connect_db():
|
||||||
return sqlite3.connect(DATABASE)
|
return sqlite3.connect(DATABASE)
|
||||||
|
|
@ -114,6 +143,11 @@ server if we run that file as standalone application::
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run()
|
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
|
.. admonition:: Troubleshooting
|
||||||
|
|
||||||
If you notice later that the browser cannot connect to the server
|
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
|
default and not every browser is happy with that. This forces IPv4
|
||||||
usage.
|
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
|
Step 3: Creating The Database
|
||||||
-----------------------------
|
-----------------------------
|
||||||
|
|
||||||
|
|
@ -159,7 +188,8 @@ first (`__future__` imports must be the very first import)::
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
Next we can create a function called `init_db` that initializes the
|
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():
|
def init_db():
|
||||||
with closing(connect_db()) as db:
|
with closing(connect_db()) as db:
|
||||||
|
|
@ -167,21 +197,26 @@ database::
|
||||||
db.cursor().executescript(f.read())
|
db.cursor().executescript(f.read())
|
||||||
db.commit()
|
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
|
Now it is possible to create a database by starting up a Python shell and
|
||||||
importing and calling that function::
|
importing and calling that function::
|
||||||
|
|
||||||
>>> from flaskr import init_db
|
>>> from flaskr import init_db
|
||||||
>>> 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
|
Step 4: Request Database Connections
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
|
@ -225,7 +260,16 @@ view functions. We will need for of them:
|
||||||
Show Entries
|
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('/')
|
@app.route('/')
|
||||||
def show_entries():
|
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
|
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
|
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'])
|
@app.route('/add', methods=['POST'])
|
||||||
def add_entry():
|
def add_entry():
|
||||||
|
|
@ -250,10 +296,19 @@ responds to `POST` requests, the actual form is shown on the
|
||||||
flash('New entry was successfully posted')
|
flash('New entry was successfully posted')
|
||||||
return redirect(url_for('show_entries'))
|
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
|
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'])
|
@app.route('/login', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
|
|
@ -269,6 +324,15 @@ These functions are used to sign the user in and out::
|
||||||
return redirect(url_for('show_entries'))
|
return redirect(url_for('show_entries'))
|
||||||
return render_template('login.html', error=error)
|
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')
|
@app.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
session.pop('logged_in', None)
|
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
|
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:
|
Put the following templates into the `templates` folder:
|
||||||
|
|
||||||
layout.html
|
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
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
@ -309,11 +392,17 @@ layout.html
|
||||||
show_entries.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
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if g.logged_in %}
|
{% if session.logged_in %}
|
||||||
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
|
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Title:
|
<dt>Title:
|
||||||
|
|
@ -336,6 +425,9 @@ show_entries.html
|
||||||
login.html
|
login.html
|
||||||
``````````
|
``````````
|
||||||
|
|
||||||
|
Finally the login template which basically just displays a form to allow
|
||||||
|
the user to login:
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
@ -352,3 +444,41 @@ login.html
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% 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.
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ class FlaskrTestCase(unittest.TestCase):
|
||||||
|
|
||||||
# testing functions
|
# 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):
|
def test_login_logout(self):
|
||||||
"""Make sure login and logout works"""
|
"""Make sure login and logout works"""
|
||||||
rv = self.login(flaskr.USERNAME, flaskr.PASSWORD)
|
rv = self.login(flaskr.USERNAME, flaskr.PASSWORD)
|
||||||
|
|
@ -46,9 +51,6 @@ class FlaskrTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def test_messages(self):
|
def test_messages(self):
|
||||||
"""Test that messages work"""
|
"""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)
|
self.login(flaskr.USERNAME, flaskr.PASSWORD)
|
||||||
rv = self.app.post('/add', data=dict(
|
rv = self.app.post('/add', data=dict(
|
||||||
title='<Hello>',
|
title='<Hello>',
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
body { font-family: sans-serif; background: #eee; }
|
body { font-family: sans-serif; background: #eee; }
|
||||||
a, h1, h2 { color: #377BA8; }
|
a, h1, h2 { color: #377BA8; }
|
||||||
h1, h2 { font-family: 'Georgia', serif; margin: 0; }
|
h1, h2 { font-family: 'Georgia', serif; margin: 0; }
|
||||||
h1 { border-bottom: 2px solid #eee; }
|
h1 { border-bottom: 2px solid #eee; }
|
||||||
h2 { font-size: 1.2em; }
|
h2 { font-size: 1.2em; }
|
||||||
|
|
||||||
div.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
|
.page { margin: 2em auto; width: 35em; border: 5px solid #ccc;
|
||||||
padding: 0.8em; background: white; }
|
padding: 0.8em; background: white; }
|
||||||
ul.entries { list-style: none; margin: 0; padding: 0; }
|
.entries { list-style: none; margin: 0; padding: 0; }
|
||||||
ul.entries li { margin: 0.8em 1.2em; }
|
.entries li { margin: 0.8em 1.2em; }
|
||||||
ul.entries li h2 { margin-left: -1em; }
|
.entries li h2 { margin-left: -1em; }
|
||||||
form.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
|
.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; }
|
||||||
form.add-entry dl { font-weight: bold; }
|
.add-entry dl { font-weight: bold; }
|
||||||
div.metanav { text-align: right; font-size: 0.8em; background: #fafafa;
|
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em;
|
||||||
padding: 0.3em; margin-bottom: 1em; }
|
margin-bottom: 1em; background: #fafafa; }
|
||||||
div.flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; }
|
.flash { background: #CEE5F5; padding: 0.5em;
|
||||||
p.error { background: #F0D6D6; padding: 0.5em; }
|
border: 1px solid #AACBE2; }
|
||||||
|
.error { background: #F0D6D6; padding: 0.5em; }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
{% if g.logged_in %}
|
{% if session.logged_in %}
|
||||||
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
|
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Title:
|
<dt>Title:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue