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