mirror of https://github.com/pallets/flask.git
New style for the documentation. Looks more like the website now.
This commit is contained in:
parent
42dd94b756
commit
bcf347fe8d
|
@ -15,13 +15,17 @@
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Georgia', serif;
|
font-family: 'Georgia', serif;
|
||||||
font-size: 16px;
|
font-size: 17px;
|
||||||
background-color: #555;
|
background-color: #ddd;
|
||||||
color: #555;
|
color: #000;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.document {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
div.documentwrapper {
|
div.documentwrapper {
|
||||||
float: left;
|
float: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -35,50 +39,57 @@ hr {
|
||||||
border: 1px solid #B1B4B6;
|
border: 1px solid #B1B4B6;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.document {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.body {
|
div.body {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
color: #3E4349;
|
color: #3E4349;
|
||||||
padding: 0 30px 30px 30px;
|
padding: 0 30px 30px 30px;
|
||||||
|
min-height: 34em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.floatingflask {
|
||||||
|
padding: 0 0 10px 10px;
|
||||||
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.footer {
|
div.footer {
|
||||||
color: #ccc;
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
margin-top: -70px;
|
||||||
|
text-align: right;
|
||||||
|
color: #888;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 0.8em;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.footer a {
|
div.footer a {
|
||||||
color: white;
|
color: #888;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.related {
|
div.related {
|
||||||
background-color: #774117;
|
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
color: #fff;
|
color: #888;
|
||||||
text-shadow: 0px 1px 0 #444;
|
}
|
||||||
font-size: 0.9em;
|
|
||||||
|
div.related ul {
|
||||||
|
padding: 0 0 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.related a {
|
div.related a {
|
||||||
color: #E9D1C1;
|
color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebar {
|
div.sphinxsidebar {
|
||||||
font-size: 0.85em;
|
font-size: 14px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebarwrapper {
|
div.sphinxsidebarwrapper {
|
||||||
padding: 20px 0 20px 0;
|
padding: 0 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebarwrapper p.logo {
|
div.sphinxsidebarwrapper p.logo {
|
||||||
padding: 0 0 10px 0;
|
padding: 20px 0 10px 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -87,39 +98,38 @@ div.sphinxsidebar h3,
|
||||||
div.sphinxsidebar h4 {
|
div.sphinxsidebar h4 {
|
||||||
font-family: 'Garamond', 'Georgia', serif;
|
font-family: 'Garamond', 'Georgia', serif;
|
||||||
color: #222;
|
color: #222;
|
||||||
font-size: 1.2em;
|
font-size: 24px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 0;
|
margin: 20px 0 5px 0;
|
||||||
padding: 5px 10px;
|
padding: 0;
|
||||||
background-color: #ddd;
|
|
||||||
text-shadow: 1px 1px 0 white
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebar h4 {
|
div.sphinxsidebar h4 {
|
||||||
font-size: 1.1em;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebar h3 a {
|
div.sphinxsidebar h3 a {
|
||||||
color: #444;
|
color: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
div.sphinxsidebar p {
|
div.sphinxsidebar p {
|
||||||
color: #555;
|
color: #555;
|
||||||
padding: 5px 20px;
|
margin: 10px 0;
|
||||||
}
|
|
||||||
|
|
||||||
div.sphinxsidebar p.topless {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebar ul {
|
div.sphinxsidebar ul {
|
||||||
margin: 10px 20px;
|
margin: 10px 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebar a {
|
div.sphinxsidebar a {
|
||||||
color: #444;
|
color: #444;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.sphinxsidebar a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebar input {
|
div.sphinxsidebar input {
|
||||||
|
@ -128,15 +138,11 @@ div.sphinxsidebar input {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.sphinxsidebar input[type=text]{
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -- body styles ----------------------------------------------------------- */
|
/* -- body styles ----------------------------------------------------------- */
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #004B6B;
|
color: #004B6B;
|
||||||
text-decoration: none;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
|
@ -144,6 +150,10 @@ a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
padding-bottom: 40px; /* saved for footer */
|
||||||
|
}
|
||||||
|
|
||||||
div.body h1,
|
div.body h1,
|
||||||
div.body h2,
|
div.body h2,
|
||||||
div.body h3,
|
div.body h3,
|
||||||
|
@ -151,20 +161,17 @@ div.body h4,
|
||||||
div.body h5,
|
div.body h5,
|
||||||
div.body h6 {
|
div.body h6 {
|
||||||
font-family: 'Garamond', 'Georiga', serif;
|
font-family: 'Garamond', 'Georiga', serif;
|
||||||
background-color: #bbb;
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: #212224;
|
|
||||||
margin: 30px 0px 10px 0px;
|
margin: 30px 0px 10px 0px;
|
||||||
padding: 5px 0 5px 10px;
|
padding: 0;
|
||||||
text-shadow: 0px 1px 0 white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
|
div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; }
|
||||||
div.body h2 { font-size: 150%; background-color: #ddd; }
|
div.body h2 { font-size: 180%; }
|
||||||
div.body h3 { font-size: 120%; background-color: #eee; }
|
div.body h3 { font-size: 150%; }
|
||||||
div.body h4 { font-size: 110%; background-color: #eee; }
|
div.body h4 { font-size: 130%; }
|
||||||
div.body h5 { font-size: 100%; background-color: #eee; }
|
div.body h5 { font-size: 100%; }
|
||||||
div.body h6 { font-size: 100%; background-color: #eee; }
|
div.body h6 { font-size: 100%; }
|
||||||
|
|
||||||
a.headerlink {
|
a.headerlink {
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -182,14 +189,24 @@ div.body p, div.body dd, div.body li {
|
||||||
}
|
}
|
||||||
|
|
||||||
div.admonition {
|
div.admonition {
|
||||||
border: 1px solid #ddd;
|
background: #fafafa;
|
||||||
background: white;
|
margin: 20px -30px;
|
||||||
-webkit-box-shadow: 2px 2px 1px #d8d8d8;
|
padding: 10px 30px;
|
||||||
-moz-box-shadow: 2px 2px 1px #d8d8d8;
|
border-top: 1px solid #ccc;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.admonition p.admonition-title + p {
|
div.admonition p.admonition-title {
|
||||||
display: inline;
|
font-family: 'Garamond', 'Georiga', serif;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.admonition p.last {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.highlight{
|
div.highlight{
|
||||||
|
@ -229,24 +246,27 @@ pre, tt {
|
||||||
}
|
}
|
||||||
|
|
||||||
img.screenshot {
|
img.screenshot {
|
||||||
-webkit-box-shadow: 4px 4px 3px #cdcdcd;
|
|
||||||
-moz-box-shadow: 4px 4px 3px #cdcdcd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tt.descname, tt.descclassname {
|
tt.descname, tt.descclassname {
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
-webkit-box-shadow: none;
|
|
||||||
-moz-box-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tt.descname {
|
tt.descname {
|
||||||
padding-right: 0.08em;
|
padding-right: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img.screenshot {
|
||||||
|
-moz-box-shadow: 2px 2px 4px #eee;
|
||||||
|
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||||
|
box-shadow: 2px 2px 4px #eee;
|
||||||
|
}
|
||||||
|
|
||||||
table.docutils {
|
table.docutils {
|
||||||
border: 1px solid #888;
|
border: 1px solid #888;
|
||||||
-webkit-box-shadow: 2px 2px 1px #d8d8d8;
|
-moz-box-shadow: 2px 2px 4px #eee;
|
||||||
-moz-box-shadow: 2px 2px 1px #d8d8d8;
|
-webkit-box-shadow: 2px 2px 4px #eee;
|
||||||
|
box-shadow: 2px 2px 4px #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.docutils td, table.docutils th {
|
table.docutils td, table.docutils th {
|
||||||
|
@ -256,15 +276,15 @@ table.docutils td, table.docutils th {
|
||||||
|
|
||||||
table.field-list, table.footnote {
|
table.field-list, table.footnote {
|
||||||
border: none;
|
border: none;
|
||||||
-webkit-box-shadow: none;
|
|
||||||
-moz-box-shadow: none;
|
-moz-box-shadow: none;
|
||||||
|
-webkit-box-shadow: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.footnote {
|
table.footnote {
|
||||||
|
margin: 15px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
|
|
||||||
-moz-box-shadow: 1px 1px 1px #d8d8d8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
table.field-list th {
|
table.field-list th {
|
||||||
|
@ -279,23 +299,36 @@ table.footnote td {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dl {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dd {
|
||||||
|
margin-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
background: #FDFDFD;
|
background: #eee;
|
||||||
padding: 10px;
|
padding: 7px 30px;
|
||||||
color: #222;
|
margin: 15px -30px;
|
||||||
line-height: 1.3em;
|
line-height: 1.3em;
|
||||||
border: 1px solid #f9f9f9;
|
}
|
||||||
margin: 1.5em 3px 1.5em 0;
|
|
||||||
-webkit-box-shadow: 2px 2px 1px #d8d8d8;
|
dl pre {
|
||||||
-moz-box-shadow: 2px 2px 1px #d8d8d8;
|
margin-left: -60px;
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dl pre {
|
||||||
|
margin-left: -90px;
|
||||||
|
padding-left: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
tt {
|
tt {
|
||||||
background-color: #ecf0f3;
|
background-color: #ecf0f3;
|
||||||
color: #222;
|
color: #222;
|
||||||
/* padding: 1px 2px; */
|
/* padding: 1px 2px; */
|
||||||
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
|
|
||||||
-moz-box-shadow: 1px 1px 1px #d8d8d8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tt.xref, a tt {
|
tt.xref, a tt {
|
||||||
|
@ -305,11 +338,3 @@ tt.xref, a tt {
|
||||||
a:hover tt {
|
a:hover tt {
|
||||||
background: #EEE;
|
background: #EEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.document + div.related {
|
|
||||||
background: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.document + div.related a {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ Welcome to Flask
|
||||||
|
|
||||||
.. image:: _static/logo-full.png
|
.. image:: _static/logo-full.png
|
||||||
:alt: The Flask Logo with Subtitle
|
:alt: The Flask Logo with Subtitle
|
||||||
:align: right
|
:class: floatingflask
|
||||||
|
|
||||||
Welcome to Flask's documentation. This documentation is divided in
|
Welcome to Flask's documentation. This documentation is divided in
|
||||||
different parts. I would suggest to get started with the
|
different parts. I would suggest to get started with the
|
||||||
|
@ -38,7 +38,7 @@ web development.
|
||||||
foreword
|
foreword
|
||||||
installation
|
installation
|
||||||
quickstart
|
quickstart
|
||||||
tutorial
|
tutorial/index
|
||||||
testing
|
testing
|
||||||
patterns
|
patterns
|
||||||
deploying
|
deploying
|
||||||
|
|
|
@ -86,10 +86,11 @@ And this is what `views.py` would look like::
|
||||||
|
|
||||||
.. admonition:: Circular Imports
|
.. admonition:: Circular Imports
|
||||||
|
|
||||||
Every Python programmer hates it, and yet we just did that: circular
|
Every Python programmer hates them, and yet we just added some:
|
||||||
imports (That's when two module depend on each one. In this case
|
circular imports (That's when two module depend on each one. In this
|
||||||
`views.py` depends on `__init__.py`). Be advised that this is a bad
|
case `views.py` depends on `__init__.py`). Be advised that this is a
|
||||||
idea in general but here it is actually fine. The reason for this is
|
bad idea in general but here it is actually fine. The reason for this
|
||||||
|
is
|
||||||
that we are not actually using the views in `__init__.py` and just
|
that we are not actually using the views in `__init__.py` and just
|
||||||
ensuring the module is imported and we are doing that at the bottom of
|
ensuring the module is imported and we are doing that at the bottom of
|
||||||
the file.
|
the file.
|
||||||
|
|
|
@ -1,484 +0,0 @@
|
||||||
.. _tutorial:
|
|
||||||
|
|
||||||
Tutorial
|
|
||||||
========
|
|
||||||
|
|
||||||
You want to develop an application with Python and Flask? Here you have
|
|
||||||
the chance to learn that by example. In this tutorial we will create a
|
|
||||||
simple microblog application. It only supports one user that can create
|
|
||||||
text-only entries and there are no feeds or comments, but it still
|
|
||||||
features everything you need to get started. We will use Flask and SQLite
|
|
||||||
as database which comes out of the box with Python, so there is nothing
|
|
||||||
else you need.
|
|
||||||
|
|
||||||
If you want the full sourcecode in advance or for comparison, check out
|
|
||||||
the `example source`_.
|
|
||||||
|
|
||||||
.. _example source:
|
|
||||||
http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/
|
|
||||||
|
|
||||||
Introducing Flaskr
|
|
||||||
------------------
|
|
||||||
|
|
||||||
We will call our blogging application flaskr here, feel free to chose a
|
|
||||||
less web-2.0-ish name ;) Basically we want it to do the following things:
|
|
||||||
|
|
||||||
1. let the user sign in and out with credentials specified in the
|
|
||||||
configuration. Only one user is supported.
|
|
||||||
2. when the user is logged in he or she can add new entries to the page
|
|
||||||
consisting of a text-only title and some HTML for the text. This HTML
|
|
||||||
is not sanitized because we trust the user here.
|
|
||||||
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
|
|
||||||
:align: center
|
|
||||||
:class: screenshot
|
|
||||||
:alt: screenshot of the final application
|
|
||||||
|
|
||||||
.. _SQLAlchemy: http://www.sqlalchemy.org/
|
|
||||||
|
|
||||||
Step 0: Creating The Folders
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
Before we get started, let's create the folders needed for this
|
|
||||||
application::
|
|
||||||
|
|
||||||
/flaskr
|
|
||||||
/static
|
|
||||||
/templates
|
|
||||||
|
|
||||||
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. 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
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
First we want to create the database schema. For this application only a
|
|
||||||
single table is needed and we only want to support SQLite so that is quite
|
|
||||||
easy. Just put the following contents into a file named `schema.sql` in
|
|
||||||
the just created `flaskr` folder:
|
|
||||||
|
|
||||||
.. sourcecode:: sql
|
|
||||||
|
|
||||||
drop table if exists entries;
|
|
||||||
create table entries (
|
|
||||||
id integer primary key autoincrement,
|
|
||||||
title string not null,
|
|
||||||
text string not null
|
|
||||||
);
|
|
||||||
|
|
||||||
This schema consists of a single table called `entries` and each row in
|
|
||||||
this table has an `id`, a `title` and a `text`. The `id` is an
|
|
||||||
automatically incrementing integer and a primary key, the other two are
|
|
||||||
strings that must not be null.
|
|
||||||
|
|
||||||
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. 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
|
|
||||||
|
|
||||||
# configuration
|
|
||||||
DATABASE = '/tmp/flaskr.db'
|
|
||||||
DEBUG = True
|
|
||||||
SECRET_KEY = 'development key'
|
|
||||||
USERNAME = 'admin'
|
|
||||||
PASSWORD = 'default'
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
Finally we just add a line to the bottom of the file that fires up the
|
|
||||||
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
|
|
||||||
during development, you might want to try this line instead::
|
|
||||||
|
|
||||||
app.run(host='127.0.0.1')
|
|
||||||
|
|
||||||
In a nutshell: Werkzeug starts up as IPv6 on many operating systems by
|
|
||||||
default and not every browser is happy with that. This forces IPv4
|
|
||||||
usage.
|
|
||||||
|
|
||||||
Step 3: Creating The Database
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
Flaskr is a database powered application as outlined earlier, and more
|
|
||||||
precisely, an application powered by a relational database system. Such
|
|
||||||
systems need a schema that tells them how to store that information. So
|
|
||||||
before starting the server for the first time it's important to create
|
|
||||||
that schema.
|
|
||||||
|
|
||||||
Such a schema can be created by piping the `schema.sql` file into the
|
|
||||||
`sqlite3` command as follows::
|
|
||||||
|
|
||||||
sqlite3 /tmp/flaskr.db < schema.sql
|
|
||||||
|
|
||||||
The downside of this is that it requires the sqlite3 command to be
|
|
||||||
installed which is not necessarily the case on every system. Also one has
|
|
||||||
to provide the path to the database there which leaves some place for
|
|
||||||
errors. It's a good idea to add a function that initializes the database
|
|
||||||
for you to the application.
|
|
||||||
|
|
||||||
If you want to do that, you first have to import the
|
|
||||||
:func:`contextlib.closing` function from the contextlib package. If you
|
|
||||||
want to use Python 2.5 it's also necessary to enable the `with` statement
|
|
||||||
first (`__future__` imports must be the very first import)::
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
from contextlib import closing
|
|
||||||
|
|
||||||
Next we can create a function called `init_db` that initializes the
|
|
||||||
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:
|
|
||||||
with app.open_resource('schema.sql') as f:
|
|
||||||
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()
|
|
||||||
|
|
||||||
Step 4: Request Database Connections
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
Now we know how we can open database connections and use them for scripts,
|
|
||||||
but how can we elegantly do that for requests? We will need the database
|
|
||||||
connection in all our functions so it makes sense to initialize them
|
|
||||||
before each request and shut them down afterwards.
|
|
||||||
|
|
||||||
Flask allows us to do that with the :meth:`~flask.Flask.before_request` and
|
|
||||||
:meth:`~flask.Flask.after_request` decorators::
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def before_request():
|
|
||||||
g.db = connect_db()
|
|
||||||
|
|
||||||
@app.after_request
|
|
||||||
def after_request(response):
|
|
||||||
g.db.close()
|
|
||||||
return response
|
|
||||||
|
|
||||||
Functions marked with :meth:`~flask.Flask.before_request` are called before
|
|
||||||
a request and passed no arguments, functions marked with
|
|
||||||
:meth:`~flask.Flask.after_request` are called after a request and
|
|
||||||
passed the response that will be sent to the client. They have to return
|
|
||||||
that response object or a different one. In this case we just return it
|
|
||||||
unchanged.
|
|
||||||
|
|
||||||
We store our current database connection on the special :data:`~flask.g`
|
|
||||||
object that flask provides for us. This object stores information for one
|
|
||||||
request only and is available from within each function. Never store such
|
|
||||||
things on other objects because this would not work with threaded
|
|
||||||
environments. That special :data:`~flask.g` object does some magic behind
|
|
||||||
the scenes to ensure it does the right thing.
|
|
||||||
|
|
||||||
Step 5: The View Functions
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
Now that the database connections are working we can start writing the
|
|
||||||
view functions. We will need four of them:
|
|
||||||
|
|
||||||
Show Entries
|
|
||||||
````````````
|
|
||||||
|
|
||||||
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():
|
|
||||||
cur = g.db.execute('select title, text from entries order by id desc')
|
|
||||||
entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
|
|
||||||
return render_template('show_entries.html', entries=entries)
|
|
||||||
|
|
||||||
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. 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():
|
|
||||||
if not session.get('logged_in'):
|
|
||||||
abort(401)
|
|
||||||
g.db.execute('insert into entries (title, text) values (?, ?)',
|
|
||||||
[request.form['title'], request.form['text']])
|
|
||||||
g.db.commit()
|
|
||||||
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. 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():
|
|
||||||
error = None
|
|
||||||
if request.method == 'POST':
|
|
||||||
if request.form['username'] != USERNAME:
|
|
||||||
error = 'Invalid username'
|
|
||||||
elif request.form['password'] != PASSWORD:
|
|
||||||
error = 'Invalid password'
|
|
||||||
else:
|
|
||||||
session['logged_in'] = True
|
|
||||||
flash('You were logged in')
|
|
||||||
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)
|
|
||||||
flash('You were logged out')
|
|
||||||
return redirect(url_for('show_entries'))
|
|
||||||
|
|
||||||
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. 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>
|
|
||||||
<title>Flaskr</title>
|
|
||||||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
|
|
||||||
<div class=page>
|
|
||||||
<h1>Flaskr</h1>
|
|
||||||
<div class=metanav>
|
|
||||||
{% if not session.logged_in %}
|
|
||||||
<a href="{{ url_for('login') }}">log in</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{{ url_for('logout') }}">log out</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% for message in get_flashed_messages() %}
|
|
||||||
<div class=flash>{{ message }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% block body %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
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 session.logged_in %}
|
|
||||||
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
|
|
||||||
<dl>
|
|
||||||
<dt>Title:
|
|
||||||
<dd><input type=text size=30 name=title>
|
|
||||||
<dt>Text:
|
|
||||||
<dd><textarea name=text rows=5 cols=40></textarea>
|
|
||||||
<dd><input type=submit value=Share>
|
|
||||||
</dl>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
<ul class=entries>
|
|
||||||
{% for entry in entries %}
|
|
||||||
<li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}
|
|
||||||
{% else %}
|
|
||||||
<li><em>Unbelievable. No entries here so far</em>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
login.html
|
|
||||||
``````````
|
|
||||||
|
|
||||||
Finally the login template which basically just displays a form to allow
|
|
||||||
the user to login:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
{% extends "layout.html" %}
|
|
||||||
{% block body %}
|
|
||||||
<h2>Login</h2>
|
|
||||||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
|
||||||
<form action="{{ url_for('login') }}" method=post>
|
|
||||||
<dl>
|
|
||||||
<dt>Username:
|
|
||||||
<dd><input type=text name=username>
|
|
||||||
<dt>Password:
|
|
||||||
<dd><input type=password name=password>
|
|
||||||
<dd><input type=submit value=Login>
|
|
||||||
</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.
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
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; }
|
|
@ -0,0 +1,33 @@
|
||||||
|
Step 4: Request Database Connections
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
Now we know how we can open database connections and use them for scripts,
|
||||||
|
but how can we elegantly do that for requests? We will need the database
|
||||||
|
connection in all our functions so it makes sense to initialize them
|
||||||
|
before each request and shut them down afterwards.
|
||||||
|
|
||||||
|
Flask allows us to do that with the :meth:`~flask.Flask.before_request` and
|
||||||
|
:meth:`~flask.Flask.after_request` decorators::
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request():
|
||||||
|
g.db = connect_db()
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
g.db.close()
|
||||||
|
return response
|
||||||
|
|
||||||
|
Functions marked with :meth:`~flask.Flask.before_request` are called before
|
||||||
|
a request and passed no arguments, functions marked with
|
||||||
|
:meth:`~flask.Flask.after_request` are called after a request and
|
||||||
|
passed the response that will be sent to the client. They have to return
|
||||||
|
that response object or a different one. In this case we just return it
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
We store our current database connection on the special :data:`~flask.g`
|
||||||
|
object that flask provides for us. This object stores information for one
|
||||||
|
request only and is available from within each function. Never store such
|
||||||
|
things on other objects because this would not work with threaded
|
||||||
|
environments. That special :data:`~flask.g` object does some magic behind
|
||||||
|
the scenes to ensure it does the right thing.
|
|
@ -0,0 +1,57 @@
|
||||||
|
Step 3: Creating The Database
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Flaskr is a database powered application as outlined earlier, and more
|
||||||
|
precisely, an application powered by a relational database system. Such
|
||||||
|
systems need a schema that tells them how to store that information. So
|
||||||
|
before starting the server for the first time it's important to create
|
||||||
|
that schema.
|
||||||
|
|
||||||
|
Such a schema can be created by piping the `schema.sql` file into the
|
||||||
|
`sqlite3` command as follows::
|
||||||
|
|
||||||
|
sqlite3 /tmp/flaskr.db < schema.sql
|
||||||
|
|
||||||
|
The downside of this is that it requires the sqlite3 command to be
|
||||||
|
installed which is not necessarily the case on every system. Also one has
|
||||||
|
to provide the path to the database there which leaves some place for
|
||||||
|
errors. It's a good idea to add a function that initializes the database
|
||||||
|
for you to the application.
|
||||||
|
|
||||||
|
If you want to do that, you first have to import the
|
||||||
|
:func:`contextlib.closing` function from the contextlib package. If you
|
||||||
|
want to use Python 2.5 it's also necessary to enable the `with` statement
|
||||||
|
first (`__future__` imports must be the very first import)::
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
Next we can create a function called `init_db` that initializes the
|
||||||
|
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:
|
||||||
|
with app.open_resource('schema.sql') as f:
|
||||||
|
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()
|
|
@ -0,0 +1,19 @@
|
||||||
|
Step 0: Creating The Folders
|
||||||
|
============================
|
||||||
|
|
||||||
|
Before we get started, let's create the folders needed for this
|
||||||
|
application::
|
||||||
|
|
||||||
|
/flaskr
|
||||||
|
/static
|
||||||
|
/templates
|
||||||
|
|
||||||
|
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. 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/
|
|
@ -0,0 +1,32 @@
|
||||||
|
.. _tutorial:
|
||||||
|
|
||||||
|
Tutorial
|
||||||
|
========
|
||||||
|
|
||||||
|
You want to develop an application with Python and Flask? Here you have
|
||||||
|
the chance to learn that by example. In this tutorial we will create a
|
||||||
|
simple microblog application. It only supports one user that can create
|
||||||
|
text-only entries and there are no feeds or comments, but it still
|
||||||
|
features everything you need to get started. We will use Flask and SQLite
|
||||||
|
as database which comes out of the box with Python, so there is nothing
|
||||||
|
else you need.
|
||||||
|
|
||||||
|
If you want the full sourcecode in advance or for comparison, check out
|
||||||
|
the `example source`_.
|
||||||
|
|
||||||
|
.. _example source:
|
||||||
|
http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
introduction
|
||||||
|
folders
|
||||||
|
schema
|
||||||
|
setup
|
||||||
|
dbcon
|
||||||
|
dbinit
|
||||||
|
views
|
||||||
|
templates
|
||||||
|
css
|
||||||
|
testing
|
|
@ -0,0 +1,29 @@
|
||||||
|
Introducing Flaskr
|
||||||
|
==================
|
||||||
|
|
||||||
|
We will call our blogging application flaskr here, feel free to chose a
|
||||||
|
less web-2.0-ish name ;) Basically we want it to do the following things:
|
||||||
|
|
||||||
|
1. let the user sign in and out with credentials specified in the
|
||||||
|
configuration. Only one user is supported.
|
||||||
|
2. when the user is logged in he or she can add new entries to the page
|
||||||
|
consisting of a text-only title and some HTML for the text. This HTML
|
||||||
|
is not sanitized because we trust the user here.
|
||||||
|
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
|
||||||
|
:align: center
|
||||||
|
:class: screenshot
|
||||||
|
:alt: screenshot of the final application
|
||||||
|
|
||||||
|
.. _SQLAlchemy: http://www.sqlalchemy.org/
|
|
@ -0,0 +1,21 @@
|
||||||
|
Step 1: Database Schema
|
||||||
|
=======================
|
||||||
|
|
||||||
|
First we want to create the database schema. For this application only a
|
||||||
|
single table is needed and we only want to support SQLite so that is quite
|
||||||
|
easy. Just put the following contents into a file named `schema.sql` in
|
||||||
|
the just created `flaskr` folder:
|
||||||
|
|
||||||
|
.. sourcecode:: sql
|
||||||
|
|
||||||
|
drop table if exists entries;
|
||||||
|
create table entries (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
title string not null,
|
||||||
|
text string not null
|
||||||
|
);
|
||||||
|
|
||||||
|
This schema consists of a single table called `entries` and each row in
|
||||||
|
this table has an `id`, a `title` and a `text`. The `id` is an
|
||||||
|
automatically incrementing integer and a primary key, the other two are
|
||||||
|
strings that must not be null.
|
|
@ -0,0 +1,69 @@
|
||||||
|
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. 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
|
||||||
|
|
||||||
|
# configuration
|
||||||
|
DATABASE = '/tmp/flaskr.db'
|
||||||
|
DEBUG = True
|
||||||
|
SECRET_KEY = 'development key'
|
||||||
|
USERNAME = 'admin'
|
||||||
|
PASSWORD = 'default'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
Finally we just add a line to the bottom of the file that fires up the
|
||||||
|
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
|
||||||
|
during development, you might want to try this line instead::
|
||||||
|
|
||||||
|
app.run(host='127.0.0.1')
|
||||||
|
|
||||||
|
In a nutshell: Werkzeug starts up as IPv6 on many operating systems by
|
||||||
|
default and not every browser is happy with that. This forces IPv4
|
||||||
|
usage.
|
|
@ -0,0 +1,107 @@
|
||||||
|
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. 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:
|
||||||
|
|
||||||
|
.. _Jinja2: http://jinja.pocoo.org/2/documentation/templates
|
||||||
|
|
||||||
|
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>
|
||||||
|
<title>Flaskr</title>
|
||||||
|
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
<div class=page>
|
||||||
|
<h1>Flaskr</h1>
|
||||||
|
<div class=metanav>
|
||||||
|
{% if not session.logged_in %}
|
||||||
|
<a href="{{ url_for('login') }}">log in</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('logout') }}">log out</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% for message in get_flashed_messages() %}
|
||||||
|
<div class=flash>{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
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 session.logged_in %}
|
||||||
|
<form action="{{ url_for('add_entry') }}" method=post class=add-entry>
|
||||||
|
<dl>
|
||||||
|
<dt>Title:
|
||||||
|
<dd><input type=text size=30 name=title>
|
||||||
|
<dt>Text:
|
||||||
|
<dd><textarea name=text rows=5 cols=40></textarea>
|
||||||
|
<dd><input type=submit value=Share>
|
||||||
|
</dl>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
<ul class=entries>
|
||||||
|
{% for entry in entries %}
|
||||||
|
<li><h2>{{ entry.title }}</h2>{{ entry.text|safe }}
|
||||||
|
{% else %}
|
||||||
|
<li><em>Unbelievable. No entries here so far</em>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
login.html
|
||||||
|
----------
|
||||||
|
|
||||||
|
Finally the login template which basically just displays a form to allow
|
||||||
|
the user to login:
|
||||||
|
|
||||||
|
.. sourcecode:: html+jinja
|
||||||
|
|
||||||
|
{% extends "layout.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<h2>Login</h2>
|
||||||
|
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %}
|
||||||
|
<form action="{{ url_for('login') }}" method=post>
|
||||||
|
<dl>
|
||||||
|
<dt>Username:
|
||||||
|
<dd><input type=text name=username>
|
||||||
|
<dt>Password:
|
||||||
|
<dd><input type=password name=password>
|
||||||
|
<dd><input type=submit value=Login>
|
||||||
|
</dl>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,9 @@
|
||||||
|
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.
|
|
@ -0,0 +1,87 @@
|
||||||
|
Step 5: The View Functions
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Now that the database connections are working we can start writing the
|
||||||
|
view functions. We will need four of them:
|
||||||
|
|
||||||
|
Show Entries
|
||||||
|
------------
|
||||||
|
|
||||||
|
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():
|
||||||
|
cur = g.db.execute('select title, text from entries order by id desc')
|
||||||
|
entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
|
||||||
|
return render_template('show_entries.html', entries=entries)
|
||||||
|
|
||||||
|
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. 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():
|
||||||
|
if not session.get('logged_in'):
|
||||||
|
abort(401)
|
||||||
|
g.db.execute('insert into entries (title, text) values (?, ?)',
|
||||||
|
[request.form['title'], request.form['text']])
|
||||||
|
g.db.commit()
|
||||||
|
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. 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():
|
||||||
|
error = None
|
||||||
|
if request.method == 'POST':
|
||||||
|
if request.form['username'] != USERNAME:
|
||||||
|
error = 'Invalid username'
|
||||||
|
elif request.form['password'] != PASSWORD:
|
||||||
|
error = 'Invalid password'
|
||||||
|
else:
|
||||||
|
session['logged_in'] = True
|
||||||
|
flash('You were logged in')
|
||||||
|
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)
|
||||||
|
flash('You were logged out')
|
||||||
|
return redirect(url_for('show_entries'))
|
Loading…
Reference in New Issue