From bcf347fe8db124242da30b97c7e570f28c273d26 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 18 Apr 2010 13:15:00 +0200 Subject: [PATCH] New style for the documentation. Looks more like the website now. --- docs/_themes/flasky/static/flasky.css_t | 183 +++++---- docs/index.rst | 4 +- docs/patterns.rst | 9 +- docs/tutorial.rst | 484 ------------------------ docs/tutorial/css.rst | 27 ++ docs/tutorial/dbcon.rst | 33 ++ docs/tutorial/dbinit.rst | 57 +++ docs/tutorial/folders.rst | 19 + docs/tutorial/index.rst | 32 ++ docs/tutorial/introduction.rst | 29 ++ docs/tutorial/schema.rst | 21 + docs/tutorial/setup.rst | 69 ++++ docs/tutorial/templates.rst | 107 ++++++ docs/tutorial/testing.rst | 9 + docs/tutorial/views.rst | 87 +++++ 15 files changed, 601 insertions(+), 569 deletions(-) delete mode 100644 docs/tutorial.rst create mode 100644 docs/tutorial/css.rst create mode 100644 docs/tutorial/dbcon.rst create mode 100644 docs/tutorial/dbinit.rst create mode 100644 docs/tutorial/folders.rst create mode 100644 docs/tutorial/index.rst create mode 100644 docs/tutorial/introduction.rst create mode 100644 docs/tutorial/schema.rst create mode 100644 docs/tutorial/setup.rst create mode 100644 docs/tutorial/templates.rst create mode 100644 docs/tutorial/testing.rst create mode 100644 docs/tutorial/views.rst diff --git a/docs/_themes/flasky/static/flasky.css_t b/docs/_themes/flasky/static/flasky.css_t index b7de089f..5a365fd9 100644 --- a/docs/_themes/flasky/static/flasky.css_t +++ b/docs/_themes/flasky/static/flasky.css_t @@ -15,13 +15,17 @@ body { font-family: 'Georgia', serif; - font-size: 16px; - background-color: #555; - color: #555; + font-size: 17px; + background-color: #ddd; + color: #000; margin: 0; padding: 0; } +div.document { + background: #fafafa; +} + div.documentwrapper { float: left; width: 100%; @@ -35,50 +39,57 @@ hr { border: 1px solid #B1B4B6; } -div.document { - background-color: #eee; -} - div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; + min-height: 34em; +} + +img.floatingflask { + padding: 0 0 10px 10px; + float: right; } div.footer { - color: #ccc; + position: absolute; + right: 0; + margin-top: -70px; + text-align: right; + color: #888; padding: 10px; - font-size: 0.8em; + font-size: 14px; } div.footer a { - color: white; + color: #888; text-decoration: underline; } div.related { - background-color: #774117; line-height: 32px; - color: #fff; - text-shadow: 0px 1px 0 #444; - font-size: 0.9em; + color: #888; +} + +div.related ul { + padding: 0 0 0 10px; } div.related a { - color: #E9D1C1; + color: #444; } div.sphinxsidebar { - font-size: 0.85em; - line-height: 1.5em; + font-size: 14px; + line-height: 1.5; } div.sphinxsidebarwrapper { - padding: 20px 0 20px 0; + padding: 0 20px; } div.sphinxsidebarwrapper p.logo { - padding: 0 0 10px 0; + padding: 20px 0 10px 0; margin: 0; text-align: center; } @@ -87,39 +98,38 @@ div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: 'Garamond', 'Georgia', serif; color: #222; - font-size: 1.2em; + font-size: 24px; font-weight: normal; - margin: 0; - padding: 5px 10px; - background-color: #ddd; - text-shadow: 1px 1px 0 white + margin: 20px 0 5px 0; + padding: 0; } div.sphinxsidebar h4 { - font-size: 1.1em; + font-size: 20px; } div.sphinxsidebar h3 a { color: #444; } - div.sphinxsidebar p { color: #555; - padding: 5px 20px; -} - -div.sphinxsidebar p.topless { + margin: 10px 0; } div.sphinxsidebar ul { - margin: 10px 20px; + margin: 10px 0; padding: 0; color: #000; } div.sphinxsidebar a { color: #444; + text-decoration: none; +} + +div.sphinxsidebar a:hover { + text-decoration: underline; } div.sphinxsidebar input { @@ -127,22 +137,22 @@ div.sphinxsidebar input { font-family: 'Georgia', serif; font-size: 1em; } - -div.sphinxsidebar input[type=text]{ - margin-left: 20px; -} /* -- body styles ----------------------------------------------------------- */ a { color: #004B6B; - text-decoration: none; + text-decoration: underline; } a:hover { color: #6D4100; text-decoration: underline; } + +div.body { + padding-bottom: 40px; /* saved for footer */ +} div.body h1, div.body h2, @@ -151,20 +161,17 @@ div.body h4, div.body h5, div.body h6 { font-family: 'Garamond', 'Georiga', serif; - background-color: #bbb; font-weight: normal; - color: #212224; margin: 30px 0px 10px 0px; - padding: 5px 0 5px 10px; - text-shadow: 0px 1px 0 white; + padding: 0; } -div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } -div.body h2 { font-size: 150%; background-color: #ddd; } -div.body h3 { font-size: 120%; background-color: #eee; } -div.body h4 { font-size: 110%; background-color: #eee; } -div.body h5 { font-size: 100%; background-color: #eee; } -div.body h6 { font-size: 100%; background-color: #eee; } +div.body h1 { margin-top: 0; padding-top: 20px; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } a.headerlink { color: white; @@ -182,14 +189,24 @@ div.body p, div.body dd, div.body li { } div.admonition { - border: 1px solid #ddd; - background: white; - -webkit-box-shadow: 2px 2px 1px #d8d8d8; - -moz-box-shadow: 2px 2px 1px #d8d8d8; + background: #fafafa; + margin: 20px -30px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; } - -div.admonition p.admonition-title + p { - display: inline; + +div.admonition p.admonition-title { + 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{ @@ -229,24 +246,27 @@ pre, tt { } img.screenshot { - -webkit-box-shadow: 4px 4px 3px #cdcdcd; - -moz-box-shadow: 4px 4px 3px #cdcdcd; } tt.descname, tt.descclassname { font-size: 0.95em; - -webkit-box-shadow: none; - -moz-box-shadow: none; } tt.descname { 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 { border: 1px solid #888; - -webkit-box-shadow: 2px 2px 1px #d8d8d8; - -moz-box-shadow: 2px 2px 1px #d8d8d8; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; } table.docutils td, table.docutils th { @@ -256,15 +276,15 @@ table.docutils td, table.docutils th { table.field-list, table.footnote { border: none; - -webkit-box-shadow: none; -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; } table.footnote { + margin: 15px 0; width: 100%; border: 1px solid #eee; - -webkit-box-shadow: 1px 1px 1px #d8d8d8; - -moz-box-shadow: 1px 1px 1px #d8d8d8; } table.field-list th { @@ -278,24 +298,37 @@ table.field-list td { table.footnote td { padding: 0.5em; } + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} pre { - background: #FDFDFD; - padding: 10px; - color: #222; + background: #eee; + padding: 7px 30px; + margin: 15px -30px; line-height: 1.3em; - border: 1px solid #f9f9f9; - margin: 1.5em 3px 1.5em 0; - -webkit-box-shadow: 2px 2px 1px #d8d8d8; - -moz-box-shadow: 2px 2px 1px #d8d8d8; +} + +dl pre { + margin-left: -60px; + padding-left: 60px; +} + +dl dl pre { + margin-left: -90px; + padding-left: 90px; } tt { background-color: #ecf0f3; color: #222; /* padding: 1px 2px; */ - -webkit-box-shadow: 1px 1px 1px #d8d8d8; - -moz-box-shadow: 1px 1px 1px #d8d8d8; } tt.xref, a tt { @@ -305,11 +338,3 @@ tt.xref, a tt { a:hover tt { background: #EEE; } - -div.document + div.related { - background: #aaa; -} - -div.document + div.related a { - color: white; -} diff --git a/docs/index.rst b/docs/index.rst index 1879070a..c0369fc3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ Welcome to Flask .. image:: _static/logo-full.png :alt: The Flask Logo with Subtitle - :align: right + :class: floatingflask Welcome to Flask's documentation. This documentation is divided in different parts. I would suggest to get started with the @@ -38,7 +38,7 @@ web development. foreword installation quickstart - tutorial + tutorial/index testing patterns deploying diff --git a/docs/patterns.rst b/docs/patterns.rst index f226b78d..b0c731e3 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -86,10 +86,11 @@ And this is what `views.py` would look like:: .. admonition:: Circular Imports - Every Python programmer hates it, and yet we just did that: circular - imports (That's when two module depend on each one. In this case - `views.py` depends on `__init__.py`). Be advised that this is a bad - idea in general but here it is actually fine. The reason for this is + Every Python programmer hates them, and yet we just added some: + circular imports (That's when two module depend on each one. In this + case `views.py` depends on `__init__.py`). Be advised that this is a + 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 ensuring the module is imported and we are doing that at the bottom of the file. diff --git a/docs/tutorial.rst b/docs/tutorial.rst deleted file mode 100644 index f642423f..00000000 --- a/docs/tutorial.rst +++ /dev/null @@ -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 - - - Flaskr - -
-

Flaskr

-
- {% if not session.logged_in %} - log in - {% else %} - log out - {% endif %} -
- {% for message in get_flashed_messages() %} -
{{ message }}
- {% endfor %} - {% block body %}{% endblock %} -
- -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 %} -
-
-
Title: -
-
Text: -
-
-
-
- {% endif %} - - {% 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 %} -

Login

- {% if error %}

Error: {{ error }}{% endif %} -

-
-
Username: -
-
Password: -
-
-
-
- {% 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. diff --git a/docs/tutorial/css.rst b/docs/tutorial/css.rst new file mode 100644 index 00000000..c2a6ba5b --- /dev/null +++ b/docs/tutorial/css.rst @@ -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; } diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst new file mode 100644 index 00000000..9741dabb --- /dev/null +++ b/docs/tutorial/dbcon.rst @@ -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. diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst new file mode 100644 index 00000000..80341d63 --- /dev/null +++ b/docs/tutorial/dbinit.rst @@ -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() diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst new file mode 100644 index 00000000..80697a94 --- /dev/null +++ b/docs/tutorial/folders.rst @@ -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/ diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst new file mode 100644 index 00000000..da37cf7a --- /dev/null +++ b/docs/tutorial/index.rst @@ -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 diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst new file mode 100644 index 00000000..04396a9d --- /dev/null +++ b/docs/tutorial/introduction.rst @@ -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/ diff --git a/docs/tutorial/schema.rst b/docs/tutorial/schema.rst new file mode 100644 index 00000000..ed329539 --- /dev/null +++ b/docs/tutorial/schema.rst @@ -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. diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst new file mode 100644 index 00000000..24b76561 --- /dev/null +++ b/docs/tutorial/setup.rst @@ -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. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst new file mode 100644 index 00000000..66b1dec6 --- /dev/null +++ b/docs/tutorial/templates.rst @@ -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 + + + Flaskr + +
+

Flaskr

+
+ {% if not session.logged_in %} + log in + {% else %} + log out + {% endif %} +
+ {% for message in get_flashed_messages() %} +
{{ message }}
+ {% endfor %} + {% block body %}{% endblock %} +
+ +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 %} +
+
+
Title: +
+
Text: +
+
+
+
+ {% endif %} + + {% 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 %} +

Login

+ {% if error %}

Error: {{ error }}{% endif %} +

+
+
Username: +
+
Password: +
+
+
+
+ {% endblock %} diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst new file mode 100644 index 00000000..3d1aa806 --- /dev/null +++ b/docs/tutorial/testing.rst @@ -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. diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst new file mode 100644 index 00000000..29be65fa --- /dev/null +++ b/docs/tutorial/views.rst @@ -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'))