diff --git a/docs/patterns.rst b/docs/patterns.rst index c7b4769a..acb6788f 100644 --- a/docs/patterns.rst +++ b/docs/patterns.rst @@ -40,6 +40,8 @@ So here a simple example how you can use SQLite 3 with Flask:: g.db.close() return response +.. _easy-querying: + Easy Querying ````````````` diff --git a/docs/quickstart.rst b/docs/quickstart.rst index df476867..1733b301 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -89,6 +89,14 @@ Or pass it to run:: Both will have exactly the same effect. +.. admonition:: Attention + + The interactive debugger however does not work in forking environments + which makes it nearly impossible to use on production servers but the + debugger still allows the execution of arbitrary code which makes it a + major security risk and **must never be used on production machines** + because of that. + Routing ------- diff --git a/docs/testing.rst b/docs/testing.rst index 4c04414d..62b309ce 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -22,72 +22,78 @@ installation. The Application --------------- -First we need an application to test for functionality. Let's start -simple with a Hello World application (`hello.py`):: +First we need an application to test for functionality. For the testing +we will use the application from the :ref:`tutorial`. If you don't have +that application yet, get the sources from `the examples`_. - from flask import Flask, render_template_string - app = Flask(__name__) - - @app.route('/') - @app.route('/') - def hello(name='World'): - return render_template_string(''' - - Hello {{ name }}! -

Hello {{ name }}!

- ''', name=name) +.. _the examples: + http://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ The Testing Skeleton -------------------- In order to test that, we add a second module ( -`hello_tests.py`) and create a unittest skeleton there:: +`flaskr_tests.py`) and create a unittest skeleton there:: import unittest - import hello + import flaskr + import tempfile - class HelloWorldTestCase(unittest.TestCase): + class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.app = hello.app.test_client() + self.db = tempfile.NamedTemporaryFile() + self.app = flaskr.app.test_client() + flaskr.DATABASE = self.db.name + flaskr.init_db() if __name__ == '__main__': unittest.main() -The code in the `setUp` function creates a new test client. That function -is called before each individual test function. What the test client does -for us is giving us a simple interface to the application. We can trigger -test requests to the application and the client will also keep track of -cookies for us. +The code in the `setUp` function creates a new test client and initialize +a new database. That function is called before each individual test function. +What the test client does for us is giving us a simple interface to the +application. We can trigger test requests to the application and the +client will also keep track of cookies for us. + +Because SQLite3 is filesystem based we can easily use the tempfile module +to create a temporary database and initialize it. Just make sure that you +keep a reference to the :class:`~tempfile.NamedTemporaryFile` around (we +store it as `self.db` because of that) so that the garbage collector does +not remove that object and with it the database from the filesystem. If we now run that testsuite, we should see the following output:: - $ python hello_tests.py + $ python flaskr_tests.py ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK -Even though it did not run any tests, we already know that our hello +Even though it did not run any tests, we already know that our flaskr application is syntactically valid, otherwise the import would have died with an exception. The First Test -------------- -Now we can add the first test. Let's check that the application greets us -with "Hello World" if we access it on ``/``. For that we modify our -created test case class so that it looks like this:: +Now we can add the first test. Let's check that the application shows +"No entries here so far" if we access the root of the application (``/``). +For that we modify our created test case class so that it looks like +this:: - class HelloWorldTestCase(unittest.TestCase): + class FlaskrTestCase(unittest.TestCase): def setUp(self): - self.app = hello.app.test_client() + self.db = tempfile.NamedTemporaryFile() + self.app = flaskr.app.test_client() + flaskr.DATABASE = self.db.name + flaskr.init_db() - def test_hello_world(self): + def test_empty_db(self): rv = self.app.get('/') - assert 'Hello World!' in rv.data + assert 'No entries here so far' in rv.data Test functions begin with the word `test`. Every function named like that will be picked up automatically. By using `self.app.get` we can send an @@ -95,22 +101,87 @@ HTTP `GET` request to the application with the given path. The return value will be a :class:`~flask.Flask.response_class` object. We can now use the :attr:`~werkzeug.BaseResponse.data` attribute to inspect the return value (as string) from the application. In this case, we ensure -that ``'Hello World!'`` is part of the output. +that ``'No entries here so far'`` is part of the output. + +Run it again and you should see one passing test:: + + $ python flaskr_tests.py + . + ---------------------------------------------------------------------- + Ran 1 test in 0.034s + + OK + +Of course you can submit forms with the test client as well which we will +use now to log our user in. + +Logging In and Out +------------------ + +The majority of the functionality of our application is only available for +the administration user. So we need a way to log our test client into the +application and out of it again. For that we fire some requests to the +login and logout pages with the required form data (username and +password). Because the login and logout pages redirect, we tell the +client to `follow_redirects`. + +Add the following two methods do your `FlaskrTestCase` class:: + + def login(self, username, password): + return self.app.post('/login', data=dict( + username=username, + password=password + ), follow_redirects=True) -Run it again and you should see one passing test. Let's add a second test -here:: + def logout(self): + return self.app.get('/logout', follow_redirects=True) - def test_hello_name(self): - rv = self.app.get('/Peter') - assert 'Hello Peter!' in rv.data +Now we can easily test if logging in and out works and that it fails with +invalid credentials. Add this as new test to the class:: + + def test_login_logout(self): + rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) + assert 'You were logged in' in rv.data + rv = self.logout() + assert 'You were logged out' in rv.data + rv = self.login(flaskr.USERNAME + 'x', flaskr.PASSWORD) + assert 'Invalid username' in rv.data + rv = self.login(flaskr.USERNAME, flaskr.PASSWORD + 'x') + assert 'Invalid password' in rv.data + +Test Adding Messages +-------------------- + +Now we can also test that adding messages works. Add a new test method +like this:: + + def test_messages(self): + self.login(flaskr.USERNAME, flaskr.PASSWORD) + rv = self.app.post('/add', data=dict( + title='', + text='HTML allowed here' + ), follow_redirects=True) + assert 'No entries here so far' not in rv.data + self.login(flaskr.USERNAME, flaskr.PASSWORD) + assert '<Hello>' in rv.data + assert 'HTML allowed here' in rv.data + +Here we also check that HTML is allowed in the text but not in the title +which is the intended behavior. + +Running that should now give us three passing tests:: + + $ python flaskr_tests.py + ... + ---------------------------------------------------------------------- + Ran 3 tests in 0.332s + + OK -Of course you can submit forms with the test client as well. For that and -other features of the test client, check the documentation of the Werkzeug -test :class:`~werkzeug.Client` and the tests of the MiniTwit example -application: +For more complex tests with headers and status codes, check out the +`MiniTwit Example`_ from the sources. That one contains a larger test +suite. -- Werkzeug Test :class:`~werkzeug.Client` -- `MiniTwit Example`_ .. _MiniTwit Example: http://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ diff --git a/docs/tutorial.rst b/docs/tutorial.rst index bbf9fa6c..bdd624c0 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -31,6 +31,13 @@ less web-2.0-ish name ;) Basically we want it to do the following things: 3. the page shows all entries so far in reverse order (newest on top) and the user can add new ones from there if logged in. +We will be using SQlite3 directly for that application because it's good +enough for an application of that size. For larger applications however +it makes a lot of sense to use `SQLAlchemy`_ that handles database +connections in a more intelligent way, allows you to target different +relational databases at once and more. You might also want to consider +one of the popular NoSQL databases if your data is more suited for those. + Here a screenshot from the final application: .. image:: _static/flaskr.png @@ -38,6 +45,8 @@ Here a screenshot from the final application: :class: screenshot :alt: screenshot of the final application +.. _SQLAlchemy: http://www.sqlalchemy.org/ + Step 0: Creating The Folders ---------------------------- @@ -50,7 +59,13 @@ application:: The `flaskr` folder is not a python package, but just something where we drop our files. Directly into this folder we will then put our database -schema as well as main module in the following steps. +schema as well as main module in the following steps. The files inside +the `static` folder are available to users of the application via `HTTP`. +This is the place where css and javascript files go. Inside the +`templates` folder Flask will look for `Jinja2`_ templates. Drop all the +templates there. + +.. _Jinja2: http://jinja.pocoo.org/2/ Step 1: Database Schema ----------------------- @@ -79,12 +94,18 @@ Step 2: Application Setup Code Now that we have the schema in place we can create the application module. Let's call it `flaskr.py` inside the `flaskr` folder. For starters we -will add the imports we will need as well as the config section:: +will add the imports we will need as well as the config section. For +small applications it's a possibility to drop the configuration directly +into the module which we will be doing here. However a cleaner solution +would be to create a separate `.ini` or `.py` file and load that or import +the values from there. + +:: # all the imports import sqlite3 - from flask import Flask, request, session, g, redirect, url_for, abort, \ - render_template, flash + from flask import Flask, request, session, g, redirect, url_for, \ + abort, render_template, flash # configuration DATABASE = '/tmp/flaskr.db' @@ -93,17 +114,25 @@ will add the imports we will need as well as the config section:: USERNAME = 'admin' PASSWORD = 'default' -The `with_statement` and :func:`~contextlib.closing` function are used to -make dealing with the database connection easier later on for setting up -the initial database. Next we can create our actual application and -initialize it with the config:: +Next we can create our actual application and initialize it with the +config:: # create our little application :) app = Flask(__name__) app.secret_key = SECRET_KEY app.debug = DEBUG -We can also add a method to easily connect to the database sepcified:: +The `secret_key` is needed to keep the client-side sessions secure. +Choose that key wisely and as hard to guess and complex as possible. The +debug flag enables or disables the interactive debugger. Never leave +debug mode activated in a production system because it will allow users to +executed code on the server! + +We also add a method to easily connect to the database specified. That +can be used to open a connection on request and also from the interactive +Python shell or a script. This will come in handy later + +:: def connect_db(): return sqlite3.connect(DATABASE) @@ -114,6 +143,11 @@ server if we run that file as standalone application:: if __name__ == '__main__': app.run() +With that out of the way you should be able to start up the application +without problems. When you head over to the server you will get an 404 +page not found error because we don't have any views yet. But we will +focus on that a little later. First we should get the database working. + .. admonition:: Troubleshooting If you notice later that the browser cannot connect to the server @@ -125,11 +159,6 @@ server if we run that file as standalone application:: default and not every browser is happy with that. This forces IPv4 usage. -With that out of the way you should be able to start up the application -without problems. When you head over to the server you will get an 404 -page not found error because we don't have any views yet. But we will -focus on that a little later. First we should get the database working. - Step 3: Creating The Database ----------------------------- @@ -159,7 +188,8 @@ first (`__future__` imports must be the very first import):: from contextlib import closing Next we can create a function called `init_db` that initializes the -database:: +database. For this we can use the `connect_db` function we defined +earlier. Just add that function below the `connect_db` function:: def init_db(): with closing(connect_db()) as db: @@ -167,21 +197,26 @@ database:: db.cursor().executescript(f.read()) db.commit() +The :func:`~contextlib.closing` helper function allows us to keep a +connection open for the duration of the `with` block. The +:func:`~flask.Flask.open_resource` method of the application object +supports that functionality out of the box, so it can be used in the +`with` block directly. This function opens a file from the resource +location (your `flaskr` folder) and allows you to read from it. We are +using this here to execute a script on the database connection. + +When we connect to a database we get a connection object (here called +`db`) that can give us a cursor. On that cursor there is a method to +execute a complete script. Finally we only have to commit the changes. +SQLite 3 and other transactional databases will not commit unless you +explicitly tell it to. + Now it is possible to create a database by starting up a Python shell and importing and calling that function:: >>> from flaskr import init_db >>> init_db() -The :meth:`~flask.Flask.open_resource` function opens a file from the -resource location (your flaskr folder) and allows you to read from it. We -are using this here to execute a script on the database connection. - -When we connect to a database we get a connection object (here called -`db`) that can give us a cursor. On that cursor there is a method to -execute a complete script. Finally we only have to commit the changes and -close the transaction. - Step 4: Request Database Connections ------------------------------------ @@ -225,7 +260,16 @@ view functions. We will need for of them: Show Entries ```````````` -This view shows all the entries stored in the database:: +This view shows all the entries stored in the database. It listens on the +root of the application and will select title and text from the database. +The one with the highest id (the newest entry) on top. The rows returned +from the cursor are tuples with the columns ordered like specified in the +select statement. This is good enough for small applications like here, +but you might want to convert them into a dict. If you are interested how +to do that, check out the :ref:`easy-querying` example. + +The view function will pass the entries as dicts to the +`show_entries.html` template and return the rendered one:: @app.route('/') def show_entries(): @@ -238,7 +282,9 @@ Add New Entry This view lets the user add new entries if he's logged in. This only responds to `POST` requests, the actual form is shown on the -`show_entries` page:: +`show_entries` page. If everything worked out well we will +:func:`~flask.flash` an information message to the next request and +redirect back to the `show_entries` page:: @app.route('/add', methods=['POST']) def add_entry(): @@ -250,10 +296,19 @@ responds to `POST` requests, the actual form is shown on the flash('New entry was successfully posted') return redirect(url_for('show_entries')) +Note that we check that the user is logged in here (the `logged_in` key is +present in the session and `True`). + Login and Logout ```````````````` -These functions are used to sign the user in and out:: +These functions are used to sign the user in and out. Login checks the +username and password against the ones from the configuration and sets the +`logged_in` key in the session. If the user logged in successfully that +key is set to `True` and the user is redirected back to the `show_entries` +page. In that case also a message is flashed that informs the user he or +she was logged in successfully. If an error occoured the template is +notified about that and the user asked again:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -269,6 +324,15 @@ These functions are used to sign the user in and out:: return redirect(url_for('show_entries')) return render_template('login.html', error=error) +The logout function on the other hand removes that key from the session +again. We use a neat trick here: if you use the :meth:`~dict.pop` method +of the dict and pass a second parameter to it (the default) the method +will delete the key from the dictionary if present or do nothing when that +key was not in there. This is helpful because we don't have to check in +that case if the user was logged in. + +:: + @app.route('/logout') def logout(): session.pop('logged_in', None) @@ -279,13 +343,32 @@ Step 6: The Templates --------------------- Now we should start working on the templates. If we request the URLs now -we would only get an exception that Flask cannot find the templates. +we would only get an exception that Flask cannot find the templates. The +templates are using `Jinja2`_ syntax and have autoescaping enabled by +default. This means that unless you mark a value in the code with +:class:`~flask.Markup` or with the ``|safe`` filter in the template, +Jinja2 will ensure that special characters such as ``<`` or ``>`` are +escaped with their XML equivalents. + +We are also using template inheritance which makes it possible to reuse +the layout of the website in all pages. Put the following templates into the `templates` folder: layout.html ``````````` +This template contains the HTML skeleton, the header and a link to log in +(or log out if the user was already logged in). It also displays the +flashed messages if there are any. The ``{% block body %}`` block can be +replaced by a block of the same name (``body``) in a child template. + +The :class:`~flask.session` dict is available in the template as well and +you can use that to check if the user is logged in or not. Note that in +Jinja you can access missing attributes and items of objects / dicts which +makes the following code work, even if there is no ``'logged_in'`` key in +the session: + .. sourcecode:: html+jinja @@ -309,11 +392,17 @@ layout.html show_entries.html ````````````````` +This template extends the `layout.html` template from above to display the +messages. Note that the `for` loop iterates over the messages we passed +in with the :func:`~flask.render_template` function. We also tell the +form to submit to your `add_entry` function and use `POST` as `HTTP` +method: + .. sourcecode:: html+jinja {% extends "layout.html" %} {% block body %} - {% if g.logged_in %} + {% if session.logged_in %}
Title: @@ -336,6 +425,9 @@ show_entries.html login.html `````````` +Finally the login template which basically just displays a form to allow +the user to login: + .. sourcecode:: html+jinja {% extends "layout.html" %} @@ -352,3 +444,41 @@ login.html
{% 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/examples/flaskr/flaskr_tests.py b/examples/flaskr/flaskr_tests.py index f8a05976..e8f01437 100644 --- a/examples/flaskr/flaskr_tests.py +++ b/examples/flaskr/flaskr_tests.py @@ -33,6 +33,11 @@ class FlaskrTestCase(unittest.TestCase): # testing functions + def test_empty_db(self): + """Start with a blank database.""" + rv = self.app.get('/') + assert 'No entries here so far' in rv.data + def test_login_logout(self): """Make sure login and logout works""" rv = self.login(flaskr.USERNAME, flaskr.PASSWORD) @@ -46,9 +51,6 @@ class FlaskrTestCase(unittest.TestCase): def test_messages(self): """Test that messages work""" - # start with a blank state - rv = self.app.get('/') - assert 'No entries here so far' in rv.data self.login(flaskr.USERNAME, flaskr.PASSWORD) rv = self.app.post('/add', data=dict( title='', diff --git a/examples/flaskr/static/style.css b/examples/flaskr/static/style.css index 39e0a8e7..4f3b71d8 100644 --- a/examples/flaskr/static/style.css +++ b/examples/flaskr/static/style.css @@ -1,17 +1,18 @@ -body { font-family: sans-serif; background: #eee; } -a, h1, h2 { color: #377BA8; } -h1, h2 { font-family: 'Georgia', serif; margin: 0; } -h1 { border-bottom: 2px solid #eee; } -h2 { font-size: 1.2em; } +body { font-family: sans-serif; background: #eee; } +a, h1, h2 { color: #377BA8; } +h1, h2 { font-family: 'Georgia', serif; margin: 0; } +h1 { border-bottom: 2px solid #eee; } +h2 { font-size: 1.2em; } -div.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; - padding: 0.8em; background: white; } -ul.entries { list-style: none; margin: 0; padding: 0; } -ul.entries li { margin: 0.8em 1.2em; } -ul.entries li h2 { margin-left: -1em; } -form.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } -form.add-entry dl { font-weight: bold; } -div.metanav { text-align: right; font-size: 0.8em; background: #fafafa; - padding: 0.3em; margin-bottom: 1em; } -div.flash { background: #CEE5F5; padding: 0.5em; border: 1px solid #AACBE2; } -p.error { background: #F0D6D6; padding: 0.5em; } +.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; + padding: 0.8em; background: white; } +.entries { list-style: none; margin: 0; padding: 0; } +.entries li { margin: 0.8em 1.2em; } +.entries li h2 { margin-left: -1em; } +.add-entry { font-size: 0.9em; border-bottom: 1px solid #ccc; } +.add-entry dl { font-weight: bold; } +.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; + margin-bottom: 1em; background: #fafafa; } +.flash { background: #CEE5F5; padding: 0.5em; + border: 1px solid #AACBE2; } +.error { background: #F0D6D6; padding: 0.5em; } diff --git a/examples/flaskr/templates/show_entries.html b/examples/flaskr/templates/show_entries.html index 55940ee7..fabe65ec 100644 --- a/examples/flaskr/templates/show_entries.html +++ b/examples/flaskr/templates/show_entries.html @@ -1,6 +1,6 @@ {% extends "layout.html" %} {% block body %} - {% if g.logged_in %} + {% if session.logged_in %}
Title: